diff --git a/README.md b/README.md index b2c3fd7..c98e7a5 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,9 @@ Commands: statuses Manage test statuses in TestRail casefields List case fields in TestRail resultfields List result fields in TestRail + priorities List test case priorities in TestRail + casetypes List test case types in TestRail + users Query users in TestRail update Update TRCLI to the latest version from PyPI. ``` @@ -2394,157 +2397,176 @@ Case Status ID: 2 Case status listing completed successfully. ``` -### Statuses Command - -The TestRail CLI provides the `statuses` command for retrieving status information from TestRail. This includes both test result statuses (Passed, Failed, Blocked, etc.) and case statuses (Approved, Draft, etc.). These statuses are crucial for mapping test results correctly and managing test case workflows. - -#### Statuses Command Overview +### Priorities Command -The `statuses` command supports two subcommands: - -| Subcommand | Purpose | API Endpoint | -|------------|---------|--------------| -| `all` | List all test result statuses | Retrieve available test result statuses (Passed, Failed, Blocked, etc.) | -| `case` | List all case statuses (Enterprise 7.3+) | Retrieve test case statuses (Approved, Draft, etc.) for case workflows | +The TestRail CLI provides the `priorities` command for retrieving all available test case priorities from TestRail. This command helps you understand the priority levels configured in your TestRail instance, including their IDs, names, short names, display order, and which priority is set as the default. #### Reference ```shell -$ trcli statuses --help +$ trcli priorities --help -Usage: trcli statuses [OPTIONS] COMMAND [ARGS]... - Manage test statuses in TestRail +Usage: trcli priorities [OPTIONS] COMMAND [ARGS]... + Manage test case priorities in TestRail Options: --help Show this message and exit. Commands: - all List all test result statuses from TestRail - case List all case statuses from TestRail (Enterprise 7.3+) + list List all test case priorities from TestRail ``` -##### Listing Test Result Statuses +#### Listing Priorities -List all test result statuses available for a project: +```shell +# List all priorities +$ trcli priorities list -h https://yourinstance.testrail.io -u user@example.com -p password + +# Show all fields including priority order +$ trcli priorities list -c config.yml --show-all-fields + +# JSON output +$ trcli priorities list -c config.yml --json-output +``` + +#### Example Output ```shell -# List all test result statuses (using config file) -$ trcli -c config.yml statuses all +$ trcli priorities list -c config.yml -# List all statuses with all parameters -$ trcli statuses all \ - --host https://yourinstance.testrail.io \ - --username \ - --password \ - --project "Your Project" +Priorities List Execution Parameters +> TestRail instance: https://yourinstance.testrail.io (user: your@email.com) +Retrieving priorities... +Found 4 priority level(s). -# Show all fields including color values -$ trcli -c config.yml statuses all --show-all-fields +ID: 1 | Name: 1 - Don't Test | Short: 1 - Don't +ID: 2 | Name: 2 - Low | Short: 2 - Low +ID: 3 | Name: 3 - Medium | Short: 3 - Med +ID: 4 | Name: 4 - Must Test | Short: 4 - Must [DEFAULT] -# JSON output for integration with other tools -$ trcli -c config.yml statuses all --json-output | jq '.[].name' +Priority listing completed successfully. ``` -**Example Output:** +**Note:** The `[DEFAULT]` marker indicates which priority is set as the default for new test cases in TestRail. Priorities are global settings and apply across all projects in your TestRail instance. + +### Case Types Command + +The TestRail CLI provides the `casetypes` command for retrieving all available test case types from TestRail. This command helps you understand the different case type classifications configured in your TestRail instance, such as Automated, Functionality, Performance, and others, including which type is set as the default. + +#### Reference ```shell -$ trcli -c config.yml statuses all +$ trcli casetypes --help -Statuses List (Test Result Statuses) Execution Parameters -> TestRail instance: https://yourinstance.testrail.io (user: test@example.com) -> Project: Your Project +Usage: trcli casetypes [OPTIONS] COMMAND [ARGS]... + Manage test case types in TestRail -Retrieving test result statuses for project ID 1... -Found 5 test result status(es): +Options: + --help Show this message and exit. -Status ID: 1 - Name: passed - Label: Passed - System Status: Yes - Is Untested: No - Is Final: Yes +Commands: + list List all test case types from TestRail +``` -Status ID: 2 - Name: blocked - Label: Blocked - System Status: Yes - Is Untested: No - Is Final: No +#### Listing Case Types -Status ID: 3 - Name: untested - Label: Untested - System Status: Yes - Is Untested: Yes - Is Final: No +```shell +# List all case types +$ trcli -h https://yourinstance.testrail.io -u user@example.com -p password casetypes list -Status ID: 4 - Name: retest - Label: Retest - System Status: Yes - Is Untested: No - Is Final: No +# With config file +$ trcli casetypes list -c config.yml -Status ID: 5 - Name: failed - Label: Failed - System Status: Yes - Is Untested: No - Is Final: Yes +# JSON output +$ trcli casetypes list -c config.yml --json-output +``` +#### Example Output -Status listing completed successfully. +```shell +$ trcli casetypes list -c config.yml + +Case Types List Execution Parameters +> TestRail instance: https://yourinstance.testrail.io (user: your@email.com) +Retrieving case types... +Found 4 case type(s). + +ID: 1 | Name: Automated +ID: 2 | Name: Functionality +ID: 3 | Name: Performance +ID: 6 | Name: Other [DEFAULT] + +Case type listing completed successfully. ``` -##### Listing Case Statuses +**Note:** The `[DEFAULT]` marker indicates which case type is set as the default for new test cases in TestRail. Case types are global settings and apply across all projects in your TestRail instance. -List all case statuses (requires TestRail Enterprise 7.3+): +### Users Command + +The TestRail CLI provides the `users` command for retrieving user information from TestRail. This command supports getting individual users by ID or email, retrieving the current authenticated user, and listing all users or users with access to a specific project. + +#### Reference ```shell -# List all case statuses (using config file) -$ trcli -c config.yml statuses case +$ trcli users --help -# List case statuses with all parameters -$ trcli statuses case \ - --host https://yourinstance.testrail.io \ - --username \ - --password \ - --project "Your Project" +Usage: trcli users [OPTIONS] COMMAND [ARGS]... + Manage users in TestRail -# Show all fields including abbreviations -$ trcli -c config.yml statuses case --show-all-fields +Options: + --help Show this message and exit. -# JSON output for integration -$ trcli -c config.yml statuses case --json-output | jq '.[].name' +Commands: + get Get a specific user from TestRail + list List all users from TestRail ``` -**Example Output:** +#### Getting a Specific User + +The `get` subcommand retrieves information about a single user using one of three mutually exclusive options: ```shell -$ trcli -c config.yml statuses case +# Get the current authenticated user +$ trcli -c config.yml users get --current -Statuses List (Case Statuses) Execution Parameters -> TestRail instance: https://yourinstance.testrail.io (user: test@example.com) -> Project: Your Project +# Get user by ID +$ trcli -c config.yml users get --user-id 5 -Retrieving case statuses... -Note: This command requires TestRail Enterprise 7.3 or later. -Found 2 case status(es): +# Get user by email +$ trcli -c config.yml users get --email john.doe@example.com -Case Status ID: 1 - Name: Approved - Is Default: No - Is Approved: Yes +# Show all fields including admin status and enterprise fields +$ trcli -c config.yml users get --user-id 1 --show-all-fields -Case Status ID: 2 - Name: Draft - Is Default: Yes - Is Approved: No +# JSON output +$ trcli -c config.yml users get --current --json-output +``` +#### Listing Users -Case status listing completed successfully. +The `list` subcommand retrieves multiple users. Without `--project-id`, it lists all users (requires administrator privileges). With `--project-id`, it lists only users with access to that specific project. + +```shell +# List all users (admin only) +$ trcli -c config.yml users list + +# List users for a specific project +$ trcli -c config.yml users list --project-id 5 + +# Show all fields for each user +$ trcli -c config.yml users list --project-id 3 --show-all-fields + +# JSON output +$ trcli -c config.yml users list --json-output ``` +**Note:** +- The `get` subcommand requires one of `--current`, `--user-id`, or `--email` (mutually exclusive) +- Listing all users without `--project-id` requires administrator privileges (TestRail 6.6+) +- When using `--project-id`, only users with explicit project access are returned (inactive users and users without project access are excluded) +- Enterprise-specific fields (SSO, assigned projects) are only available in TestRail Enterprise +- The `--show-all-fields` option displays additional information including admin status, groups, MFA requirements, and enterprise fields + ### Case Fields Command The TestRail CLI provides the `casefields` command for retrieving all available test case custom fields from TestRail. This command helps you understand what custom fields are available for test cases, their types, configurations, and which projects they apply to. diff --git a/tests/test_cmd_casetypes.py b/tests/test_cmd_casetypes.py new file mode 100644 index 0000000..18ec37d --- /dev/null +++ b/tests/test_cmd_casetypes.py @@ -0,0 +1,119 @@ +import pytest +from unittest import mock +from unittest.mock import MagicMock, patch +from click.testing import CliRunner + +from trcli.cli import Environment +from trcli.commands import cmd_casetypes + + +class TestCmdCaseTypes: + """Test class for casetypes command functionality""" + + def setup_method(self): + """Set up test environment""" + self.runner = CliRunner() + self.environment = Environment(cmd="casetypes") + self.environment.host = "https://test.testrail.com" + self.environment.username = "test@example.com" + self.environment.password = "password" + self.environment.api_key = None + + def _setup_project_client_mock(self, mock_project_client): + """Helper to setup ProjectBasedClient mock""" + mock_client_instance = MagicMock() + mock_project_client.return_value = mock_client_instance + return mock_client_instance + + @mock.patch("trcli.commands.cmd_casetypes.ProjectBasedClient") + def test_list_case_types_success(self, mock_project_client): + """Test successful case types listing""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.case_type_handler.get_case_types.return_value = ( + [ + {"id": 1, "is_default": False, "name": "Automated"}, + {"id": 2, "is_default": False, "name": "Functionality"}, + {"id": 3, "is_default": False, "name": "Performance"}, + {"id": 6, "is_default": True, "name": "Other"}, + ], + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_casetypes.list, [], obj=self.environment) + + assert result.exit_code == 0 + mock_client.api_request_handler.case_type_handler.get_case_types.assert_called_once() + assert mock_log.called + + @mock.patch("trcli.commands.cmd_casetypes.ProjectBasedClient") + def test_list_case_types_json_output(self, mock_project_client): + """Test case types listing with JSON output""" + mock_client = self._setup_project_client_mock(mock_project_client) + case_types_data = [ + {"id": 1, "is_default": False, "name": "Automated"}, + {"id": 6, "is_default": True, "name": "Other"}, + ] + mock_client.api_request_handler.case_type_handler.get_case_types.return_value = (case_types_data, "") + + with patch.object(self.environment, "set_parameters"), patch.object( + self.environment, "check_for_required_parameters" + ): + result = self.runner.invoke(cmd_casetypes.list, ["--json-output"], obj=self.environment) + + assert result.exit_code == 0 + # Check for prettified JSON + assert '"name": "Automated"' in result.output + assert "\n" in result.output + + @mock.patch("trcli.commands.cmd_casetypes.ProjectBasedClient") + def test_list_case_types_empty_result(self, mock_project_client): + """Test case types listing with no case types (edge case)""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.case_type_handler.get_case_types.return_value = ([], "") + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_casetypes.list, [], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + + @mock.patch("trcli.commands.cmd_casetypes.ProjectBasedClient") + def test_list_case_types_api_error(self, mock_project_client): + """Test case types listing with API error""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.case_type_handler.get_case_types.return_value = ([], "Connection timeout") + + with patch.object(self.environment, "elog") as mock_elog, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_casetypes.list, [], obj=self.environment) + + assert result.exit_code == 1 + mock_elog.assert_called_with("Error: Failed to retrieve case types: Connection timeout") + + @mock.patch("trcli.commands.cmd_casetypes.ProjectBasedClient") + def test_list_case_types_default_marked(self, mock_project_client): + """Test that default case type is properly marked""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.case_type_handler.get_case_types.return_value = ( + [ + {"id": 1, "is_default": False, "name": "Automated"}, + {"id": 6, "is_default": True, "name": "Other"}, + ], + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_casetypes.list, [], obj=self.environment) + + assert result.exit_code == 0 + # Verify DEFAULT marker appears for the default case type + log_calls = [str(call) for call in mock_log.call_args_list] + assert any("[DEFAULT]" in str(call) for call in log_calls) diff --git a/tests/test_cmd_priorities.py b/tests/test_cmd_priorities.py new file mode 100644 index 0000000..28b4f4b --- /dev/null +++ b/tests/test_cmd_priorities.py @@ -0,0 +1,199 @@ +import pytest +from unittest import mock +from unittest.mock import MagicMock, patch +from click.testing import CliRunner + +from trcli.cli import Environment +from trcli.commands import cmd_priorities + + +class TestCmdPriorities: + """Test class for priorities command functionality""" + + def setup_method(self): + """Set up test environment""" + self.runner = CliRunner() + self.environment = Environment(cmd="priorities") + self.environment.host = "https://test.testrail.com" + self.environment.username = "test@example.com" + self.environment.password = "password" + self.environment.api_key = None + + def _setup_project_client_mock(self, mock_project_client): + """Helper to setup ProjectBasedClient mock""" + mock_client_instance = MagicMock() + mock_project_client.return_value = mock_client_instance + return mock_client_instance + + @mock.patch("trcli.commands.cmd_priorities.ProjectBasedClient") + def test_list_priorities_success(self, mock_project_client): + """Test successful priorities listing""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.priority_handler.get_priorities.return_value = ( + [ + { + "id": 1, + "is_default": False, + "name": "1 - Don't Test", + "priority": 1, + "short_name": "1 - Don't", + }, + { + "id": 2, + "is_default": False, + "name": "2 - Low", + "priority": 2, + "short_name": "2 - Low", + }, + { + "id": 3, + "is_default": False, + "name": "3 - Medium", + "priority": 3, + "short_name": "3 - Med", + }, + { + "id": 4, + "is_default": True, + "name": "4 - Must Test", + "priority": 4, + "short_name": "4 - Must", + }, + ], + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_priorities.list, [], obj=self.environment) + + assert result.exit_code == 0 + mock_client.api_request_handler.priority_handler.get_priorities.assert_called_once() + assert mock_log.called + + @mock.patch("trcli.commands.cmd_priorities.ProjectBasedClient") + def test_list_priorities_json_output(self, mock_project_client): + """Test priorities listing with JSON output""" + mock_client = self._setup_project_client_mock(mock_project_client) + priorities_data = [ + { + "id": 1, + "is_default": False, + "name": "1 - Don't Test", + "priority": 1, + "short_name": "1 - Don't", + }, + { + "id": 4, + "is_default": True, + "name": "4 - Must Test", + "priority": 4, + "short_name": "4 - Must", + }, + ] + mock_client.api_request_handler.priority_handler.get_priorities.return_value = (priorities_data, "") + + with patch.object(self.environment, "set_parameters"), patch.object( + self.environment, "check_for_required_parameters" + ): + result = self.runner.invoke(cmd_priorities.list, ["--json-output"], obj=self.environment) + + assert result.exit_code == 0 + # Check for prettified JSON + assert '"name": "4 - Must Test"' in result.output + assert "\n" in result.output + + @mock.patch("trcli.commands.cmd_priorities.ProjectBasedClient") + def test_list_priorities_show_all_fields(self, mock_project_client): + """Test priorities listing with show all fields""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.priority_handler.get_priorities.return_value = ( + [ + { + "id": 1, + "is_default": False, + "name": "1 - Don't Test", + "priority": 1, + "short_name": "1 - Don't", + }, + { + "id": 4, + "is_default": True, + "name": "4 - Must Test", + "priority": 4, + "short_name": "4 - Must", + }, + ], + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_priorities.list, ["--show-all-fields"], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + + @mock.patch("trcli.commands.cmd_priorities.ProjectBasedClient") + def test_list_priorities_empty_result(self, mock_project_client): + """Test priorities listing with no priorities (edge case)""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.priority_handler.get_priorities.return_value = ([], "") + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_priorities.list, [], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + + @mock.patch("trcli.commands.cmd_priorities.ProjectBasedClient") + def test_list_priorities_api_error(self, mock_project_client): + """Test priorities listing with API error""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.priority_handler.get_priorities.return_value = ([], "Connection timeout") + + with patch.object(self.environment, "elog") as mock_elog, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_priorities.list, [], obj=self.environment) + + assert result.exit_code == 1 + mock_elog.assert_called_with("Error: Failed to retrieve priorities: Connection timeout") + + @mock.patch("trcli.commands.cmd_priorities.ProjectBasedClient") + def test_list_priorities_default_marked(self, mock_project_client): + """Test that default priority is properly marked""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.priority_handler.get_priorities.return_value = ( + [ + { + "id": 2, + "is_default": False, + "name": "2 - Low", + "priority": 2, + "short_name": "2 - Low", + }, + { + "id": 3, + "is_default": True, + "name": "3 - Medium", + "priority": 3, + "short_name": "3 - Med", + }, + ], + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_priorities.list, [], obj=self.environment) + + assert result.exit_code == 0 + # Verify DEFAULT marker appears for the default priority + log_calls = [str(call) for call in mock_log.call_args_list] + assert any("[DEFAULT]" in str(call) for call in log_calls) diff --git a/tests/test_cmd_users.py b/tests/test_cmd_users.py new file mode 100644 index 0000000..3f37d98 --- /dev/null +++ b/tests/test_cmd_users.py @@ -0,0 +1,262 @@ +import pytest +from unittest import mock +from unittest.mock import MagicMock, patch +from click.testing import CliRunner + +from trcli.cli import Environment +from trcli.commands import cmd_users + + +class TestCmdUsers: + """Test class for users command functionality""" + + def setup_method(self): + """Set up test environment""" + self.runner = CliRunner() + self.environment = Environment(cmd="users") + self.environment.host = "https://test.testrail.com" + self.environment.username = "test@example.com" + self.environment.password = "password" + self.environment.api_key = None + + def _setup_project_client_mock(self, mock_project_client): + """Helper to setup ProjectBasedClient mock""" + mock_client_instance = MagicMock() + mock_project_client.return_value = mock_client_instance + return mock_client_instance + + # GET subcommand tests + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_get_current_user_success(self, mock_project_client): + """Test getting current authenticated user""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.user_handler.get_current_user.return_value = ( + { + "id": 1, + "email": "test@example.com", + "is_active": True, + "name": "Test User", + "role_id": 3, + "role": "Tester", + }, + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.get, ["--current"], obj=self.environment) + + assert result.exit_code == 0 + mock_client.api_request_handler.user_handler.get_current_user.assert_called_once() + assert mock_log.called + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_get_user_by_id_success(self, mock_project_client): + """Test getting user by ID""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.user_handler.get_user.return_value = ( + {"id": 5, "email": "john.doe@example.com", "name": "John Doe", "role": "Tester", "is_active": True}, + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.get, ["--user-id", "5"], obj=self.environment) + + assert result.exit_code == 0 + mock_client.api_request_handler.user_handler.get_user.assert_called_once_with(5) + assert mock_log.called + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_get_user_by_email_success(self, mock_project_client): + """Test getting user by email""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.user_handler.get_user_by_email.return_value = ( + {"id": 3, "email": "jane@example.com", "name": "Jane Smith", "role": "Lead", "is_active": True}, + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.get, ["--email", "jane@example.com"], obj=self.environment) + + assert result.exit_code == 0 + mock_client.api_request_handler.user_handler.get_user_by_email.assert_called_once_with("jane@example.com") + assert mock_log.called + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_get_user_with_all_fields(self, mock_project_client): + """Test getting user with all fields displayed""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.user_handler.get_user.return_value = ( + { + "id": 1, + "email": "admin@example.com", + "name": "Admin User", + "role": "Administrator", + "role_id": 1, + "is_active": True, + "is_admin": True, + "group_ids": [1, 2, 3], + "mfa_required": True, + "email_notifications": True, + "sso_enabled": False, + "assigned_projects": [1, 3, 5], + }, + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.get, ["--user-id", "1", "--show-all-fields"], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_get_user_json_output(self, mock_project_client): + """Test getting user with JSON output""" + mock_client = self._setup_project_client_mock(mock_project_client) + user_data = {"id": 2, "email": "user@example.com", "name": "Test User", "role": "Tester"} + mock_client.api_request_handler.user_handler.get_user.return_value = (user_data, "") + + with patch.object(self.environment, "set_parameters"), patch.object( + self.environment, "check_for_required_parameters" + ): + result = self.runner.invoke(cmd_users.get, ["--user-id", "2", "--json-output"], obj=self.environment) + + assert result.exit_code == 0 + assert '"email": "user@example.com"' in result.output + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_get_user_no_option_error(self, mock_project_client): + """Test error when no option is provided""" + self._setup_project_client_mock(mock_project_client) + + with patch.object(self.environment, "elog") as mock_elog, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.get, [], obj=self.environment) + + assert result.exit_code == 1 + mock_elog.assert_called_with("Error: Must specify one of --current, --user-id, or --email") + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_get_user_multiple_options_error(self, mock_project_client): + """Test error when multiple exclusive options are provided""" + self._setup_project_client_mock(mock_project_client) + + with patch.object(self.environment, "elog") as mock_elog, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.get, ["--current", "--user-id", "5"], obj=self.environment) + + assert result.exit_code == 1 + mock_elog.assert_called_with("Error: Options --current, --user-id, and --email are mutually exclusive") + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_get_user_api_error(self, mock_project_client): + """Test getting user with API error""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.user_handler.get_user.return_value = (None, "User not found") + + with patch.object(self.environment, "elog") as mock_elog, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.get, ["--user-id", "999"], obj=self.environment) + + assert result.exit_code == 1 + mock_elog.assert_called_with("Error: Failed to retrieve user: User not found") + + # LIST subcommand tests + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_list_all_users_success(self, mock_project_client): + """Test listing all users""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.user_handler.get_users.return_value = ( + [ + {"id": 1, "name": "John Doe", "email": "john@example.com"}, + {"id": 2, "name": "Jane Smith", "email": "jane@example.com"}, + {"id": 3, "name": "Bob Johnson", "email": "bob@example.com"}, + ], + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.list, [], obj=self.environment) + + assert result.exit_code == 0 + mock_client.api_request_handler.user_handler.get_users.assert_called_once_with(None) + assert mock_log.called + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_list_users_by_project_success(self, mock_project_client): + """Test listing users filtered by project""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.user_handler.get_users.return_value = ( + [ + {"id": 1, "name": "John Doe", "role": "Tester", "role_id": 3}, + {"id": 2, "name": "Jane Smith", "role": "Lead", "role_id": 2}, + ], + "", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.list, ["--project-id", "5"], obj=self.environment) + + assert result.exit_code == 0 + mock_client.api_request_handler.user_handler.get_users.assert_called_once_with(5) + assert mock_log.called + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_list_users_json_output(self, mock_project_client): + """Test listing users with JSON output""" + mock_client = self._setup_project_client_mock(mock_project_client) + users_data = [{"id": 1, "name": "John Doe"}, {"id": 2, "name": "Jane Smith"}] + mock_client.api_request_handler.user_handler.get_users.return_value = (users_data, "") + + with patch.object(self.environment, "set_parameters"), patch.object( + self.environment, "check_for_required_parameters" + ): + result = self.runner.invoke(cmd_users.list, ["--json-output"], obj=self.environment) + + assert result.exit_code == 0 + assert '"name": "John Doe"' in result.output + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_list_users_empty_result(self, mock_project_client): + """Test listing users with empty result""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.user_handler.get_users.return_value = ([], "") + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.list, [], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + + @mock.patch("trcli.commands.cmd_users.ProjectBasedClient") + def test_list_users_api_error(self, mock_project_client): + """Test listing users with API error""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.user_handler.get_users.return_value = ([], "Permission denied") + + with patch.object(self.environment, "elog") as mock_elog, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"): + result = self.runner.invoke(cmd_users.list, [], obj=self.environment) + + assert result.exit_code == 1 + mock_elog.assert_called_with("Error: Failed to retrieve users: Permission denied") diff --git a/tests/test_data/cli_test_data.py b/tests/test_data/cli_test_data.py index 4094e2b..ab9871f 100644 --- a/tests/test_data/cli_test_data.py +++ b/tests/test_data/cli_test_data.py @@ -83,6 +83,9 @@ " - statuses: Query test statuses\n" " - casefields: Query test case custom fields\n" " - resultfields: Query test result custom fields\n" + " - priorities: Query test case priorities\n" + " - casetypes: Query test case types\n" + " - users: Query users (get and list)\n" " - results: Query and update test results (list, update)\n" ) diff --git a/trcli/api/api_request_handler.py b/trcli/api/api_request_handler.py index ddfe02b..d4bc03e 100644 --- a/trcli/api/api_request_handler.py +++ b/trcli/api/api_request_handler.py @@ -21,6 +21,9 @@ from trcli.api.status_handler import StatusHandler from trcli.api.case_field_handler import CaseFieldHandler from trcli.api.result_field_handler import ResultFieldHandler +from trcli.api.priority_handler import PriorityHandler +from trcli.api.case_type_handler import CaseTypeHandler +from trcli.api.user_handler import UserHandler from trcli.cli import Environment from trcli.constants import ( ProjectErrors, @@ -96,6 +99,9 @@ def __init__( self.status_handler = StatusHandler(api_client) self.case_field_handler = CaseFieldHandler(api_client) self.result_field_handler = ResultFieldHandler(api_client) + self.priority_handler = PriorityHandler(api_client) + self.case_type_handler = CaseTypeHandler(api_client) + self.user_handler = UserHandler(api_client) # BDD case cache for feature name matching (shared by CucumberParser and JunitParser) # Structure: {"{project_id}_{suite_id}": {normalized_name: [case_dict, case_dict, ...]}} diff --git a/trcli/api/case_type_handler.py b/trcli/api/case_type_handler.py new file mode 100644 index 0000000..74d5744 --- /dev/null +++ b/trcli/api/case_type_handler.py @@ -0,0 +1,38 @@ +""" +Handler for TestRail case type operations. +Provides methods to retrieve test case type information. +""" + +from typing import List, Tuple + + +class CaseTypeHandler: + """Handles case type-related API operations.""" + + def __init__(self, api_client): + """ + Initialize CaseTypeHandler. + + Args: + api_client: APIClient instance for making API calls + """ + self.api_client = api_client + + def get_case_types(self) -> Tuple[List[dict], str]: + """ + Get all available test case types. + + Returns: + Tuple of (list of case type dictionaries, error message) + Case type dict contains: id, name, is_default + """ + response = self.api_client.send_get("get_case_types") + + if response.error_message: + return [], response.error_message + + case_types = response.response_text + if not isinstance(case_types, list): + return [], "Invalid response format: expected list of case types" + + return case_types, "" diff --git a/trcli/api/priority_handler.py b/trcli/api/priority_handler.py new file mode 100644 index 0000000..219a27f --- /dev/null +++ b/trcli/api/priority_handler.py @@ -0,0 +1,38 @@ +""" +Handler for TestRail priority operations. +Provides methods to retrieve test case priority information. +""" + +from typing import List, Tuple + + +class PriorityHandler: + """Handles priority-related API operations.""" + + def __init__(self, api_client): + """ + Initialize PriorityHandler. + + Args: + api_client: APIClient instance for making API calls + """ + self.api_client = api_client + + def get_priorities(self) -> Tuple[List[dict], str]: + """ + Get all available test case priorities. + + Returns: + Tuple of (list of priority dictionaries, error message) + Priority dict contains: id, name, short_name, priority, is_default + """ + response = self.api_client.send_get("get_priorities") + + if response.error_message: + return [], response.error_message + + priorities = response.response_text + if not isinstance(priorities, list): + return [], "Invalid response format: expected list of priorities" + + return priorities, "" diff --git a/trcli/api/user_handler.py b/trcli/api/user_handler.py new file mode 100644 index 0000000..6256170 --- /dev/null +++ b/trcli/api/user_handler.py @@ -0,0 +1,115 @@ +""" +Handler for TestRail user operations. +Provides methods to retrieve user information. +""" + +from typing import List, Tuple, Dict, Union, Optional + + +class UserHandler: + """Handles user-related API operations.""" + + def __init__(self, api_client): + """ + Initialize UserHandler. + + Args: + api_client: APIClient instance for making API calls + """ + self.api_client = api_client + + def get_user(self, user_id: int) -> Tuple[Optional[Dict], str]: + """ + Get a specific user by ID. + + Args: + user_id: The ID of the user + + Returns: + Tuple of (user dictionary, error message) + User dict contains: id, email, name, role, role_id, is_active, is_admin, etc. + """ + response = self.api_client.send_get(f"get_user/{user_id}") + + if response.error_message: + return None, response.error_message + + user = response.response_text + if not isinstance(user, dict): + return None, "Invalid response format: expected user dictionary" + + return user, "" + + def get_current_user(self) -> Tuple[Optional[Dict], str]: + """ + Get the current authenticated user. + + Returns: + Tuple of (user dictionary, error message) + User dict contains: id, email, name, role, role_id, is_active + """ + response = self.api_client.send_get("get_current_user") + + if response.error_message: + return None, response.error_message + + user = response.response_text + if not isinstance(user, dict): + return None, "Invalid response format: expected user dictionary" + + return user, "" + + def get_user_by_email(self, email: str) -> Tuple[Optional[Dict], str]: + """ + Get a specific user by email address. + + Args: + email: The email address of the user + + Returns: + Tuple of (user dictionary, error message) + User dict contains: id, email, name, role, role_id, is_active, is_admin, etc. + """ + response = self.api_client.send_get(f"get_user_by_email&email={email}") + + if response.error_message: + return None, response.error_message + + user = response.response_text + if not isinstance(user, dict): + return None, "Invalid response format: expected user dictionary" + + return user, "" + + def get_users(self, project_id: Optional[int] = None) -> Tuple[List[Dict], str]: + """ + Get all users or users for a specific project. + + Args: + project_id: Optional project ID to filter users by project access + + Returns: + Tuple of (list of user dictionaries, error message) + User dict contains: id, name, email (if no project), role, role_id (if project specified) + """ + if project_id: + endpoint = f"get_users/{project_id}" + else: + endpoint = "get_users" + + response = self.api_client.send_get(endpoint) + + if response.error_message: + return [], response.error_message + + response_data = response.response_text + + # Handle paginated response format (with 'users' key) or direct list + if isinstance(response_data, dict) and "users" in response_data: + users = response_data["users"] + elif isinstance(response_data, list): + users = response_data + else: + return [], "Invalid response format: expected list of users or paginated response" + + return users, "" diff --git a/trcli/commands/cmd_casetypes.py b/trcli/commands/cmd_casetypes.py new file mode 100644 index 0000000..174cfe3 --- /dev/null +++ b/trcli/commands/cmd_casetypes.py @@ -0,0 +1,75 @@ +import builtins +import click +import json + +from trcli.api.project_based_client import ProjectBasedClient +from trcli.cli import pass_environment, CONTEXT_SETTINGS, Environment +from trcli.data_classes.dataclass_testrail import TestRailSuite + + +def print_config(env: Environment, action: str): + env.log(f"Case Types {action} Execution Parameters" f"\n> TestRail instance: {env.host} (user: {env.username})") + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.pass_context +@pass_environment +def cli(environment: Environment, context: click.Context, *args, **kwargs): + """Manage test case types in TestRail""" + environment.cmd = "casetypes" + environment.set_parameters(context) + + +@cli.command() +@click.option("--json-output", is_flag=True, help="Output case types as raw JSON from API.") +@click.pass_context +@pass_environment +def list( + environment: Environment, + context: click.Context, + json_output: bool, + *args, + **kwargs, +): + """List all test case types from TestRail""" + environment.check_for_required_parameters() + + print_config(environment, "List") + + # Create ProjectBasedClient (case types are global, but we use this for consistent API access) + project_client = ProjectBasedClient( + environment=environment, + suite=TestRailSuite(name=environment.suite_name, suite_id=environment.suite_id), + ) + + environment.log("Retrieving case types...") + + # Retrieve case types using CaseTypeHandler + case_types, error_message = project_client.api_request_handler.case_type_handler.get_case_types() + + if error_message: + environment.elog(f"Error: Failed to retrieve case types: {error_message}") + raise SystemExit(1) + + if json_output: + # Output prettified JSON response + print(json.dumps(case_types, indent=2)) + else: + environment.log(f"Found {len(case_types)} case type(s).") + environment.log("") + + if not case_types: + environment.log("No case types found.") + else: + # Display case types in a simple format + for case_type in case_types: + case_type_id = case_type.get("id") + name = case_type.get("name", "N/A") + is_default = case_type.get("is_default", False) + + default_marker = " [DEFAULT]" if is_default else "" + environment.log(f"ID: {case_type_id} | Name: {name}{default_marker}") + + environment.log("") + + environment.log("Case type listing completed successfully.") diff --git a/trcli/commands/cmd_priorities.py b/trcli/commands/cmd_priorities.py new file mode 100644 index 0000000..0660b28 --- /dev/null +++ b/trcli/commands/cmd_priorities.py @@ -0,0 +1,86 @@ +import builtins +import click +import json + +from trcli.api.project_based_client import ProjectBasedClient +from trcli.cli import pass_environment, CONTEXT_SETTINGS, Environment +from trcli.data_classes.dataclass_testrail import TestRailSuite + + +def print_config(env: Environment, action: str): + env.log(f"Priorities {action} Execution Parameters" f"\n> TestRail instance: {env.host} (user: {env.username})") + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.pass_context +@pass_environment +def cli(environment: Environment, context: click.Context, *args, **kwargs): + """Manage test case priorities in TestRail""" + environment.cmd = "priorities" + environment.set_parameters(context) + + +@cli.command() +@click.option("--json-output", is_flag=True, help="Output priorities as raw JSON from API.") +@click.option("--show-all-fields", is_flag=True, help="Show all fields including priority order and default flag.") +@click.pass_context +@pass_environment +def list( + environment: Environment, + context: click.Context, + json_output: bool, + show_all_fields: bool, + *args, + **kwargs, +): + """List all test case priorities from TestRail""" + environment.check_for_required_parameters() + + print_config(environment, "List") + + # Create ProjectBasedClient (priorities are global, but we use this for consistent API access) + project_client = ProjectBasedClient( + environment=environment, + suite=TestRailSuite(name=environment.suite_name, suite_id=environment.suite_id), + ) + + environment.log("Retrieving priorities...") + + # Retrieve priorities using PriorityHandler + priorities, error_message = project_client.api_request_handler.priority_handler.get_priorities() + + if error_message: + environment.elog(f"Error: Failed to retrieve priorities: {error_message}") + raise SystemExit(1) + + if json_output: + # Output prettified JSON response + print(json.dumps(priorities, indent=2)) + else: + environment.log(f"Found {len(priorities)} priority level(s).") + environment.log("") + + if not priorities: + environment.log("No priorities found.") + else: + # Display priorities in a table-like format + for priority in priorities: + priority_id = priority.get("id") + name = priority.get("name", "N/A") + short_name = priority.get("short_name", "N/A") + is_default = priority.get("is_default", False) + priority_order = priority.get("priority", "N/A") + + default_marker = " [DEFAULT]" if is_default else "" + + if show_all_fields: + environment.log( + f"ID: {priority_id} | Name: {name} | Short: {short_name} | " + f"Order: {priority_order}{default_marker}" + ) + else: + environment.log(f"ID: {priority_id} | Name: {name} | Short: {short_name}{default_marker}") + + environment.log("") + + environment.log("Priority listing completed successfully.") diff --git a/trcli/commands/cmd_users.py b/trcli/commands/cmd_users.py new file mode 100644 index 0000000..e89ff26 --- /dev/null +++ b/trcli/commands/cmd_users.py @@ -0,0 +1,184 @@ +import builtins +import click +import json + +from trcli.api.project_based_client import ProjectBasedClient +from trcli.cli import pass_environment, CONTEXT_SETTINGS, Environment +from trcli.data_classes.dataclass_testrail import TestRailSuite + + +def print_config(env: Environment, action: str): + env.log(f"Users {action} Execution Parameters" f"\n> TestRail instance: {env.host} (user: {env.username})") + + +def display_user(env: Environment, user: dict, show_all_fields: bool = False): + """Helper function to display a single user's information.""" + env.log(f"User ID: {user.get('id')}") + env.log(f" Name: {user.get('name', 'N/A')}") + env.log(f" Email: {user.get('email', 'N/A')}") + + if show_all_fields: + env.log(f" Role: {user.get('role', 'N/A')}") + env.log(f" Role ID: {user.get('role_id', 'N/A')}") + env.log(f" Active: {'Yes' if user.get('is_active') else 'No'}") + env.log(f" Admin: {'Yes' if user.get('is_admin') else 'No'}") + + # Enterprise-specific fields + if "sso_enabled" in user: + env.log(f" SSO Enabled: {'Yes' if user.get('sso_enabled') else 'No'}") + if "assigned_projects" in user: + env.log(f" Assigned Projects: {user.get('assigned_projects', [])}") + if "group_ids" in user: + env.log(f" Group IDs: {user.get('group_ids', [])}") + if "mfa_required" in user: + env.log(f" MFA Required: {'Yes' if user.get('mfa_required') else 'No'}") + if "email_notifications" in user: + env.log(f" Email Notifications: {'Yes' if user.get('email_notifications') else 'No'}") + else: + # Basic display + if "role" in user: + env.log(f" Role: {user.get('role', 'N/A')}") + if "is_active" in user: + env.log(f" Active: {'Yes' if user.get('is_active') else 'No'}") + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.pass_context +@pass_environment +def cli(environment: Environment, context: click.Context, *args, **kwargs): + """Manage users in TestRail""" + environment.cmd = "users" + environment.set_parameters(context) + + +@cli.command() +@click.option("--current", is_flag=True, help="Get the current authenticated user.") +@click.option("--user-id", type=int, metavar="", help="Get user by user ID.") +@click.option("--email", type=str, metavar="", help="Get user by email address.") +@click.option("--json-output", is_flag=True, help="Output user as raw JSON from API.") +@click.option( + "--show-all-fields", is_flag=True, help="Show all fields including admin status, groups, and enterprise fields." +) +@click.pass_context +@pass_environment +def get( + environment: Environment, + context: click.Context, + current: bool, + user_id: int, + email: str, + json_output: bool, + show_all_fields: bool, + *args, + **kwargs, +): + """Get a specific user from TestRail""" + environment.check_for_required_parameters() + + # Validate mutually exclusive options + options_provided = sum([current, user_id is not None, email is not None]) + if options_provided == 0: + environment.elog("Error: Must specify one of --current, --user-id, or --email") + raise SystemExit(1) + if options_provided > 1: + environment.elog("Error: Options --current, --user-id, and --email are mutually exclusive") + raise SystemExit(1) + + print_config(environment, "Get") + + # Create ProjectBasedClient for consistent API access + project_client = ProjectBasedClient( + environment=environment, + suite=TestRailSuite(name=environment.suite_name, suite_id=environment.suite_id), + ) + + # Retrieve user based on option + user = None + error_message = "" + + if current: + environment.log("Retrieving current authenticated user...") + user, error_message = project_client.api_request_handler.user_handler.get_current_user() + elif user_id: + environment.log(f"Retrieving user with ID {user_id}...") + user, error_message = project_client.api_request_handler.user_handler.get_user(user_id) + elif email: + environment.log(f"Retrieving user with email {email}...") + user, error_message = project_client.api_request_handler.user_handler.get_user_by_email(email) + + if error_message: + environment.elog(f"Error: Failed to retrieve user: {error_message}") + raise SystemExit(1) + + if not user: + environment.log("No user found.") + return + + if json_output: + print(json.dumps(user, indent=2)) + else: + environment.log("") + display_user(environment, user, show_all_fields) + environment.log("") + + environment.log("User retrieval completed successfully.") + + +@cli.command() +@click.option( + "--project-id", + type=int, + metavar="", + help="Filter users by project ID (returns only users with access to the project).", +) +@click.option("--json-output", is_flag=True, help="Output users as raw JSON from API.") +@click.option("--show-all-fields", is_flag=True, help="Show all fields for each user.") +@click.pass_context +@pass_environment +def list( + environment: Environment, + context: click.Context, + project_id: int, + json_output: bool, + show_all_fields: bool, + *args, + **kwargs, +): + """List all users from TestRail""" + environment.check_for_required_parameters() + + print_config(environment, "List") + + # Create ProjectBasedClient for consistent API access + project_client = ProjectBasedClient( + environment=environment, + suite=TestRailSuite(name=environment.suite_name, suite_id=environment.suite_id), + ) + + if project_id: + environment.log(f"Retrieving users for project ID {project_id}...") + else: + environment.log("Retrieving all users...") + environment.log("Note: Listing all users requires administrator privileges.") + + # Retrieve users + users, error_message = project_client.api_request_handler.user_handler.get_users(project_id) + + if error_message: + environment.elog(f"Error: Failed to retrieve users: {error_message}") + raise SystemExit(1) + + if json_output: + print(json.dumps(users, indent=2)) + else: + environment.log(f"Found {len(users)} user(s).") + environment.log("") + + if not users: + environment.log("No users found.") + else: + for user in users: + display_user(environment, user, show_all_fields) + environment.log("") + + environment.log("User listing completed successfully.") diff --git a/trcli/constants.py b/trcli/constants.py index 504374c..e8c996d 100644 --- a/trcli/constants.py +++ b/trcli/constants.py @@ -100,6 +100,9 @@ statuses=dict(**FAULT_MAPPING), casefields=dict(**FAULT_MAPPING), resultfields=dict(**FAULT_MAPPING), + priorities=dict(**FAULT_MAPPING), + casetypes=dict(**FAULT_MAPPING), + users=dict(**FAULT_MAPPING), ) PROMPT_MESSAGES = dict( @@ -136,6 +139,9 @@ - statuses: Query test statuses - casefields: Query test case custom fields - resultfields: Query test result custom fields + - priorities: Query test case priorities + - casetypes: Query test case types + - users: Query users (get and list) - results: Query and update test results (list, update)""" MISSING_COMMAND_SLOGAN = """Usage: trcli [OPTIONS] COMMAND [ARGS]...\nTry 'trcli --help' for help.