diff --git a/README.md b/README.md index b2c3fd7..f184b33 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,252 @@ Case Status ID: 2 Case status listing completed successfully. ``` -### Statuses Command +### Priorities 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. +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. -#### Statuses Command Overview +#### Reference -The `statuses` command supports two subcommands: +```shell +$ trcli priorities --help -| 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 | +Usage: trcli priorities [OPTIONS] COMMAND [ARGS]... + Manage test case priorities in TestRail + +Options: + --help Show this message and exit. + +Commands: + list List all test case priorities from TestRail +``` + +#### Listing Priorities + +```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 +$ trcli priorities list -c config.yml + +Priorities List Execution Parameters +> TestRail instance: https://yourinstance.testrail.io (user: your@email.com) +Retrieving priorities... +Found 4 priority level(s). + +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] + +Priority listing completed successfully. +``` + +**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 statuses --help +$ trcli casetypes --help -Usage: trcli statuses [OPTIONS] COMMAND [ARGS]... - Manage test statuses in TestRail +Usage: trcli casetypes [OPTIONS] COMMAND [ARGS]... + Manage test case types 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 types from TestRail ``` -##### Listing Test Result Statuses +#### Listing Case Types -List all test result statuses available for a project: +```shell +# List all case types +$ trcli -h https://yourinstance.testrail.io -u user@example.com -p password casetypes list + +# With config file +$ trcli casetypes list -c config.yml + +# JSON output +$ trcli casetypes list -c config.yml --json-output +``` + +#### Example Output ```shell -# List all test result statuses (using config file) -$ trcli -c config.yml statuses all +$ trcli casetypes list -c config.yml -# List all statuses with all parameters -$ trcli statuses all \ - --host https://yourinstance.testrail.io \ - --username \ - --password \ - --project "Your Project" +Case Types List Execution Parameters +> TestRail instance: https://yourinstance.testrail.io (user: your@email.com) +Retrieving case types... +Found 4 case type(s). -# Show all fields including color values -$ trcli -c config.yml statuses all --show-all-fields +ID: 1 | Name: Automated +ID: 2 | Name: Functionality +ID: 3 | Name: Performance +ID: 6 | Name: Other [DEFAULT] -# JSON output for integration with other tools -$ trcli -c config.yml statuses all --json-output | jq '.[].name' +Case type listing completed successfully. ``` -**Example Output:** +**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. + +### 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 -$ trcli -c config.yml statuses all +$ trcli users --help -Statuses List (Test Result Statuses) Execution Parameters -> TestRail instance: https://yourinstance.testrail.io (user: test@example.com) -> Project: Your Project +Usage: trcli users [OPTIONS] COMMAND [ARGS]... + Manage users 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: + get Get a specific user from TestRail + list List all users from TestRail +``` -Status ID: 2 - Name: blocked - Label: Blocked - System Status: Yes - Is Untested: No - Is Final: No +#### Getting a Specific User -Status ID: 3 - Name: untested - Label: Untested - System Status: Yes - Is Untested: Yes - Is Final: No +The `get` subcommand retrieves information about a single user using one of three mutually exclusive options: -Status ID: 4 - Name: retest - Label: Retest - System Status: Yes - Is Untested: No - Is Final: No +```shell +# Get the current authenticated user +$ trcli -c config.yml users get --current -Status ID: 5 - Name: failed - Label: Failed - System Status: Yes - Is Untested: No - Is Final: Yes +# Get user by ID +$ trcli -c config.yml users get --user-id 5 +# Get user by email +$ trcli -c config.yml users get --email john.doe@example.com -Status listing completed successfully. +# Show all fields including admin status and enterprise fields +$ trcli -c config.yml users get --user-id 1 --show-all-fields + +# JSON output +$ trcli -c config.yml users get --current --json-output ``` -##### Listing Case Statuses +#### Listing Users -List all case statuses (requires TestRail Enterprise 7.3+): +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 case statuses (using config file) -$ trcli -c config.yml statuses case +# List all users (admin only) +$ trcli -c config.yml users list -# List case statuses with all parameters -$ trcli statuses case \ - --host https://yourinstance.testrail.io \ - --username \ - --password \ - --project "Your Project" +# List users for a specific project +$ trcli -c config.yml users list --project-id 5 -# Show all fields including abbreviations -$ trcli -c config.yml statuses case --show-all-fields +# Show all fields for each user +$ trcli -c config.yml users list --project-id 3 --show-all-fields -# JSON output for integration -$ trcli -c config.yml statuses case --json-output | jq '.[].name' +# JSON output +$ trcli -c config.yml users list --json-output ``` -**Example 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 + +### Projects Command + +The `projects` command provides functionality to query and retrieve project information from TestRail. This command allows you to get details about specific projects or list all projects in your TestRail instance. + +The `projects` command supports two subcommands: +- **get**: Retrieve detailed information about a specific project by ID +- **list**: List all projects or filter by completion status with pagination support + +#### Getting a Specific Project + +The `get` subcommand retrieves detailed information about a single project using its project ID. ```shell -$ trcli -c config.yml statuses case +# Get a specific project by ID +$ trcli -c config.yml projects get --project-id 1 -Statuses List (Case Statuses) Execution Parameters -> TestRail instance: https://yourinstance.testrail.io (user: test@example.com) -> Project: Your Project +# Get project with all fields (includes users, groups, announcement) +$ trcli -c config.yml projects get --project-id 1 --show-all-fields -Retrieving case statuses... -Note: This command requires TestRail Enterprise 7.3 or later. -Found 2 case status(es): +# JSON output +$ trcli -c config.yml projects get --project-id 1 --json-output +``` -Case Status ID: 1 - Name: Approved - Is Default: No - Is Approved: Yes +#### Listing Projects -Case Status ID: 2 - Name: Draft - Is Default: Yes - Is Approved: No +The `list` subcommand retrieves multiple projects. You can list all projects or filter by completion status, and use pagination to manage large numbers of projects. + +```shell +# List all projects +$ trcli -c config.yml projects list +# List only active projects +$ trcli -c config.yml projects list --is-completed 0 -Case status listing completed successfully. +# List only completed projects +$ trcli -c config.yml projects list --is-completed 1 + +# List with pagination +$ trcli -c config.yml projects list --limit 10 --offset 0 + +# Show all fields for each project +$ trcli -c config.yml projects list --show-all-fields + +# JSON output +$ trcli -c config.yml projects list --json-output ``` +**Note:** +- The `--is-completed` filter accepts 0 for active projects or 1 for completed projects +- Pagination parameters `--limit` and `--offset` allow you to retrieve projects in manageable batches +- The `--show-all-fields` option displays additional information including announcement, users, groups, and default role + +### Templates Command + +The `templates` command provides functionality to query available templates (field layouts) for test cases in a TestRail project. Templates define which fields are available when creating or editing test cases, such as "Test Case (Text)", "Test Case (Steps)", "Exploratory Session", "Behaviour Driven Development", or "AI Evaluation". + +The `templates` command supports one subcommand: +- **list**: List all templates available for a specific project + +#### Listing Templates + +The `list` subcommand retrieves all templates (field layouts) for a specific project. + +```shell +# List all templates for a project +$ trcli -c config.yml templates list --project-id 1 + +# JSON output +$ trcli -c config.yml templates list --project-id 1 --json-output +``` + +**Note:** +- The `--project-id` parameter is required and must be a valid project ID (x>=1) +- Each template has a unique ID, name, and indicates whether it's the default template for the project +- Templates control which fields are available when creating test cases + ### 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_projects.py b/tests/test_cmd_projects.py new file mode 100644 index 0000000..b7a0fc6 --- /dev/null +++ b/tests/test_cmd_projects.py @@ -0,0 +1,302 @@ +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_projects + + +class TestCmdProjects: + """Test suite for the projects command.""" + + def setup_method(self): + """Set up test environment""" + self.runner = CliRunner() + self.environment = Environment(cmd="projects") + 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_projects.ProjectBasedClient") + def test_get_project_success(self, mock_project_client): + """Test successful project retrieval by ID.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_project.return_value = ( + {"id": 1, "name": "Test Project", "is_completed": False, "suite_mode": 3}, + "", + ) + + 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_projects.get, ["--project-id", "1"], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + mock_client.api_request_handler.project_handler.get_project.assert_called_once_with(1) + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_get_project_with_all_fields(self, mock_project_client): + """Test project retrieval with all fields displayed.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_project.return_value = ( + { + "id": 1, + "name": "Test Project", + "announcement": "Welcome!", + "completed_on": 1389968184, + "default_role_id": 3, + "default_role": "Tester", + "is_completed": False, + "show_announcement": True, + "suite_mode": 3, + "url": "https://test.testrail.io/projects/1", + "users": [{"id": 5}], + "groups": [], + }, + "", + ) + + 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_projects.get, ["--project-id", "1", "--show-all-fields"], obj=self.environment + ) + + assert result.exit_code == 0 + assert mock_log.called + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_get_project_json_output(self, mock_project_client): + """Test project retrieval with JSON output.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_project.return_value = ( + {"id": 1, "name": "Test Project", "is_completed": False, "suite_mode": 3}, + "", + ) + + 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_projects.get, ["--project-id", "1", "--json-output"], obj=self.environment) + + assert result.exit_code == 0 + assert '"id": 1' in result.output + assert '"name": "Test Project"' in result.output + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_get_project_api_error(self, mock_project_client): + """Test handling of API errors when getting a project.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_project.return_value = ( + None, + "Project not found", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"), patch.object( + self.environment, "elog" + ) as mock_elog: + result = self.runner.invoke(cmd_projects.get, ["--project-id", "999"], obj=self.environment) + + assert result.exit_code == 1 + assert mock_elog.called + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_list_projects_success(self, mock_project_client): + """Test successful listing of all projects.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_projects.return_value = ( + [ + {"id": 1, "name": "Project A", "is_completed": False, "suite_mode": 1}, + {"id": 2, "name": "Project B", "is_completed": True, "suite_mode": 3}, + ], + "", + ) + + 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_projects.list, [], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + mock_client.api_request_handler.project_handler.get_projects.assert_called_once_with( + is_completed=None, limit=None, offset=None + ) + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_list_projects_with_filters(self, mock_project_client): + """Test listing projects with filters (is_completed, limit, offset).""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_projects.return_value = ( + [{"id": 1, "name": "Active Project", "is_completed": False, "suite_mode": 1}], + "", + ) + + 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_projects.list, ["--is-completed", "0", "--limit", "10", "--offset", "5"], obj=self.environment + ) + + assert result.exit_code == 0 + assert mock_log.called + mock_client.api_request_handler.project_handler.get_projects.assert_called_once_with( + is_completed=0, limit=10, offset=5 + ) + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_list_projects_completed_only(self, mock_project_client): + """Test listing only completed projects.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_projects.return_value = ( + [{"id": 2, "name": "Completed Project", "is_completed": True, "suite_mode": 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_projects.list, ["--is-completed", "1"], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + mock_client.api_request_handler.project_handler.get_projects.assert_called_once_with( + is_completed=1, limit=None, offset=None + ) + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_list_projects_json_output(self, mock_project_client): + """Test listing projects with JSON output.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_projects.return_value = ( + [ + {"id": 1, "name": "Project A", "is_completed": False, "suite_mode": 1}, + {"id": 2, "name": "Project B", "is_completed": True, "suite_mode": 3}, + ], + "", + ) + + 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_projects.list, ["--json-output"], obj=self.environment) + + assert result.exit_code == 0 + assert '"id": 1' in result.output + assert '"name": "Project A"' in result.output + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_list_projects_empty_result(self, mock_project_client): + """Test listing projects when no results are found.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_projects.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_projects.list, [], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_list_projects_api_error(self, mock_project_client): + """Test handling of API errors when listing projects.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_projects.return_value = ( + [], + "Connection error", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"), patch.object( + self.environment, "elog" + ) as mock_elog: + result = self.runner.invoke(cmd_projects.list, [], obj=self.environment) + + assert result.exit_code == 1 + assert mock_elog.called + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_list_projects_with_pagination(self, mock_project_client): + """Test listing projects with pagination parameters.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_projects.return_value = ( + [ + {"id": 11, "name": "Project 11", "is_completed": False, "suite_mode": 1}, + {"id": 12, "name": "Project 12", "is_completed": False, "suite_mode": 1}, + ], + "", + ) + + 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_projects.list, ["--limit", "2", "--offset", "10"], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + mock_client.api_request_handler.project_handler.get_projects.assert_called_once_with( + is_completed=None, limit=2, offset=10 + ) + + def test_get_project_invalid_id_zero(self): + """Test validation rejects project ID of 0.""" + result = self.runner.invoke(cmd_projects.get, ["--project-id", "0"], obj=self.environment) + + assert result.exit_code != 0 + assert "0 is not in the range x>=1" in result.output + + def test_get_project_invalid_id_negative(self): + """Test validation rejects negative project ID.""" + result = self.runner.invoke(cmd_projects.get, ["--project-id", "-1"], obj=self.environment) + + assert result.exit_code != 0 + assert "-1 is not in the range x>=1" in result.output + + @mock.patch("trcli.commands.cmd_projects.ProjectBasedClient") + def test_get_project_with_groups(self, mock_project_client): + """Test project retrieval with groups displayed.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.project_handler.get_project.return_value = ( + { + "id": 1, + "name": "Test Project", + "is_completed": False, + "suite_mode": 3, + "users": [{"user_id": 2, "global_role": "Tester", "project_role": "Tester"}], + "groups": [ + {"id": 1, "role": "Tester", "role_id": 3}, + {"id": 2, "role": "Designer", "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_projects.get, ["--project-id", "1", "--show-all-fields"], obj=self.environment + ) + + assert result.exit_code == 0 + assert mock_log.called + # Verify groups are mentioned in logs + log_calls = [str(call) for call in mock_log.call_args_list] + groups_mentioned = any("Groups: 2 group(s)" in str(call) for call in log_calls) + assert groups_mentioned diff --git a/tests/test_cmd_templates.py b/tests/test_cmd_templates.py new file mode 100644 index 0000000..60d9919 --- /dev/null +++ b/tests/test_cmd_templates.py @@ -0,0 +1,152 @@ +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_templates + + +class TestCmdTemplates: + """Test suite for the templates command.""" + + def setup_method(self): + """Set up test environment""" + self.runner = CliRunner() + self.environment = Environment(cmd="templates") + 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_templates.ProjectBasedClient") + def test_list_templates_success(self, mock_project_client): + """Test successful listing of templates.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.template_handler.get_templates.return_value = ( + [ + {"id": 1, "name": "Test Case (Text)", "i18n_custom_id": "templates_test_case_text", "is_default": True}, + { + "id": 2, + "name": "Test Case (Steps)", + "i18n_custom_id": "templates_test_case_steps", + "is_default": False, + }, + {"id": 5, "name": "AI Evaluation", "i18n_custom_id": "templates_ai_evaluation", "is_default": False}, + ], + "", + ) + + 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_templates.list, ["--project-id", "1"], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + mock_client.api_request_handler.template_handler.get_templates.assert_called_once_with(1) + + @mock.patch("trcli.commands.cmd_templates.ProjectBasedClient") + def test_list_templates_json_output(self, mock_project_client): + """Test listing templates with JSON output.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.template_handler.get_templates.return_value = ( + [ + {"id": 1, "name": "Test Case (Text)", "i18n_custom_id": "templates_test_case_text", "is_default": True}, + { + "id": 2, + "name": "Test Case (Steps)", + "i18n_custom_id": "templates_test_case_steps", + "is_default": False, + }, + ], + "", + ) + + 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_templates.list, ["--project-id", "1", "--json-output"], obj=self.environment + ) + + assert result.exit_code == 0 + assert '"id": 1' in result.output + assert '"name": "Test Case (Text)"' in result.output + + @mock.patch("trcli.commands.cmd_templates.ProjectBasedClient") + def test_list_templates_empty_result(self, mock_project_client): + """Test listing templates when no results are found.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.template_handler.get_templates.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_templates.list, ["--project-id", "1"], obj=self.environment) + + assert result.exit_code == 0 + assert mock_log.called + + @mock.patch("trcli.commands.cmd_templates.ProjectBasedClient") + def test_list_templates_api_error(self, mock_project_client): + """Test handling of API errors when listing templates.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.template_handler.get_templates.return_value = ( + [], + "Project not found", + ) + + with patch.object(self.environment, "log") as mock_log, patch.object( + self.environment, "set_parameters" + ), patch.object(self.environment, "check_for_required_parameters"), patch.object( + self.environment, "elog" + ) as mock_elog: + result = self.runner.invoke(cmd_templates.list, ["--project-id", "999"], obj=self.environment) + + assert result.exit_code == 1 + assert mock_elog.called + + def test_list_templates_invalid_id_zero(self): + """Test validation rejects project ID of 0.""" + result = self.runner.invoke(cmd_templates.list, ["--project-id", "0"], obj=self.environment) + + assert result.exit_code != 0 + assert "0 is not in the range x>=1" in result.output + + def test_list_templates_invalid_id_negative(self): + """Test validation rejects negative project ID.""" + result = self.runner.invoke(cmd_templates.list, ["--project-id", "-1"], obj=self.environment) + + assert result.exit_code != 0 + assert "-1 is not in the range x>=1" in result.output + + @mock.patch("trcli.commands.cmd_templates.ProjectBasedClient") + def test_list_templates_displays_default_flag(self, mock_project_client): + """Test that templates correctly show default flag.""" + mock_client = self._setup_project_client_mock(mock_project_client) + mock_client.api_request_handler.template_handler.get_templates.return_value = ( + [ + {"id": 1, "name": "Test Case (Text)", "i18n_custom_id": "templates_test_case_text", "is_default": True}, + {"id": 5, "name": "AI Evaluation", "i18n_custom_id": "templates_ai_evaluation", "is_default": False}, + ], + "", + ) + + 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_templates.list, ["--project-id", "1"], obj=self.environment) + + assert result.exit_code == 0 + # Verify default flag is mentioned in logs + log_calls = [str(call) for call in mock_log.call_args_list] + default_yes = any("Default: Yes" in str(call) for call in log_calls) + default_no = any("Default: No" in str(call) for call in log_calls) + assert default_yes and default_no 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..234f3c4 100644 --- a/tests/test_data/cli_test_data.py +++ b/tests/test_data/cli_test_data.py @@ -83,6 +83,11 @@ " - 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" + " - projects: Query projects (get and list)\n" + " - templates: Query templates (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..5de2e9a 100644 --- a/trcli/api/api_request_handler.py +++ b/trcli/api/api_request_handler.py @@ -21,6 +21,11 @@ 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.api.project_handler import ProjectHandler +from trcli.api.template_handler import TemplateHandler from trcli.cli import Environment from trcli.constants import ( ProjectErrors, @@ -96,6 +101,11 @@ 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) + self.project_handler = ProjectHandler(api_client) + self.template_handler = TemplateHandler(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/project_handler.py b/trcli/api/project_handler.py new file mode 100644 index 0000000..0c4cb85 --- /dev/null +++ b/trcli/api/project_handler.py @@ -0,0 +1,87 @@ +""" +Handler for TestRail project operations. +Provides methods to retrieve project information. +""" + +from typing import List, Tuple, Dict, Optional + + +class ProjectHandler: + """Handles project-related API operations.""" + + def __init__(self, api_client): + """ + Initialize ProjectHandler. + + Args: + api_client: APIClient instance for making API calls + """ + self.api_client = api_client + + def get_project(self, project_id: int) -> Tuple[Optional[Dict], str]: + """ + Get a specific project by ID. + + Args: + project_id: The ID of the project + + Returns: + Tuple of (project dictionary, error message) + Project dict contains: id, name, announcement, completed_on, is_completed, + suite_mode, show_announcement, url, users, groups, default_role_id, default_role + """ + response = self.api_client.send_get(f"get_project/{project_id}") + + if response.error_message: + return None, response.error_message + + project = response.response_text + if not isinstance(project, dict): + return None, "Invalid response format: expected project dictionary" + + return project, "" + + def get_projects( + self, is_completed: Optional[int] = None, limit: Optional[int] = None, offset: Optional[int] = None + ) -> Tuple[List[Dict], str]: + """ + Get all projects with optional filters. + + Args: + is_completed: Optional filter - 1 for completed projects, 0 for active projects + limit: Optional limit for pagination (default: 250) + offset: Optional offset for pagination (default: 0) + + Returns: + Tuple of (list of project dictionaries, error message) + Project dict contains: id, name, announcement, is_completed, suite_mode, etc. + """ + # Build endpoint with query parameters + params = [] + if is_completed is not None: + params.append(f"is_completed={is_completed}") + if limit is not None: + params.append(f"limit={limit}") + if offset is not None: + params.append(f"offset={offset}") + + endpoint = "get_projects" + if params: + endpoint += "&" + "&".join(params) + + 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 'projects' key) or direct list + if isinstance(response_data, dict) and "projects" in response_data: + projects = response_data["projects"] + elif isinstance(response_data, list): + projects = response_data + else: + return [], "Invalid response format: expected list of projects or paginated response" + + return projects, "" diff --git a/trcli/api/template_handler.py b/trcli/api/template_handler.py new file mode 100644 index 0000000..0fbb521 --- /dev/null +++ b/trcli/api/template_handler.py @@ -0,0 +1,42 @@ +""" +Handler for TestRail template operations. +Provides methods to retrieve template information. +""" + +from typing import List, Tuple, Dict + + +class TemplateHandler: + """Handles template-related API operations.""" + + def __init__(self, api_client): + """ + Initialize TemplateHandler. + + Args: + api_client: APIClient instance for making API calls + """ + self.api_client = api_client + + def get_templates(self, project_id: int) -> Tuple[List[Dict], str]: + """ + Get all templates (field layouts) for a project. + + Args: + project_id: The ID of the project + + Returns: + Tuple of (list of template dictionaries, error message) + Template dict contains: id, name, i18n_custom_id, is_default + """ + response = self.api_client.send_get(f"get_templates/{project_id}") + + if response.error_message: + return [], response.error_message + + templates = response.response_text + + if not isinstance(templates, list): + return [], "Invalid response format: expected list of templates" + + return templates, "" 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_projects.py b/trcli/commands/cmd_projects.py new file mode 100644 index 0000000..02db2de --- /dev/null +++ b/trcli/commands/cmd_projects.py @@ -0,0 +1,182 @@ +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"Projects {action} Execution Parameters" f"\n> TestRail instance: {env.host} (user: {env.username})") + + +def display_project(env: Environment, project: dict, show_all_fields: bool = False): + """Helper function to display a single project's information.""" + env.log(f"Project ID: {project.get('id')}") + env.log(f" Name: {project.get('name', 'N/A')}") + env.log(f" Completed: {'Yes' if project.get('is_completed') else 'No'}") + env.log(f" Suite Mode: {project.get('suite_mode', 'N/A')}") + + if show_all_fields: + env.log(f" URL: {project.get('url', 'N/A')}") + env.log(f" Show Announcement: {'Yes' if project.get('show_announcement') else 'No'}") + if project.get("announcement"): + env.log(f" Announcement: {project.get('announcement')}") + if project.get("completed_on"): + env.log(f" Completed On: {project.get('completed_on')}") + env.log(f" Default Role: {project.get('default_role', 'N/A')} (ID: {project.get('default_role_id', 'N/A')})") + + # Display users if available + users = project.get("users", []) + if users: + env.log(f" Users: {len(users)} user(s)") + for user in users[:5]: # Show first 5 users + user_id = user.get("user_id") + global_role = user.get("global_role", "N/A") + project_role = user.get("project_role", "N/A") + env.log(f" - User ID: {user_id}, Global Role: {global_role}, Project Role: {project_role}") + if len(users) > 5: + env.log(f" ... and {len(users) - 5} more") + + # Display groups if available + groups = project.get("groups", []) + if groups: + env.log(f" Groups: {len(groups)} group(s)") + for group in groups[:5]: # Show first 5 groups + group_id = group.get("id") + role = group.get("role", "N/A") + role_id = group.get("role_id", "N/A") + env.log(f" - Group ID: {group_id}, Role: {role} (Role ID: {role_id})") + if len(groups) > 5: + env.log(f" ... and {len(groups) - 5} more") + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.pass_context +@pass_environment +def cli(environment: Environment, context: click.Context, *args, **kwargs): + """Manage projects in TestRail""" + environment.cmd = "projects" + environment.set_parameters(context) + + +@cli.command() +@click.option( + "--project-id", type=click.IntRange(min=1), metavar="", required=True, help="Get project by project ID." +) +@click.option("--json-output", is_flag=True, help="Output project as raw JSON from API.") +@click.option("--show-all-fields", is_flag=True, help="Show all fields including announcement, users, and groups.") +@click.pass_context +@pass_environment +def get( + environment: Environment, + context: click.Context, + project_id: int, + json_output: bool, + show_all_fields: bool, + *args, + **kwargs, +): + """Get a specific project from TestRail""" + environment.check_for_required_parameters() + + 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 project + environment.log(f"Retrieving project with ID {project_id}...") + project, error_message = project_client.api_request_handler.project_handler.get_project(project_id) + + if error_message: + environment.elog(f"Error: Failed to retrieve project: {error_message}") + raise SystemExit(1) + + if not project: + environment.log("No project found.") + return + + if json_output: + print(json.dumps(project, indent=2)) + else: + environment.log("") + display_project(environment, project, show_all_fields) + environment.log("") + + environment.log("Project retrieval completed successfully.") + + +@cli.command() +@click.option( + "--is-completed", + type=int, + metavar="<0|1>", + help="Filter projects by completion status (0=active, 1=completed).", +) +@click.option("--limit", type=int, metavar="", help="Limit number of projects returned (default: 250).") +@click.option("--offset", type=int, metavar="", help="Offset for pagination (default: 0).") +@click.option("--json-output", is_flag=True, help="Output projects as raw JSON from API.") +@click.option("--show-all-fields", is_flag=True, help="Show all fields for each project.") +@click.pass_context +@pass_environment +def list( + environment: Environment, + context: click.Context, + is_completed: int, + limit: int, + offset: int, + json_output: bool, + show_all_fields: bool, + *args, + **kwargs, +): + """List all projects 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), + ) + + # Build description of filters + filters = [] + if is_completed is not None: + filters.append(f"completed={is_completed}") + if limit is not None: + filters.append(f"limit={limit}") + if offset is not None: + filters.append(f"offset={offset}") + + filter_desc = f" with filters: {', '.join(filters)}" if filters else "" + environment.log(f"Retrieving projects{filter_desc}...") + + # Retrieve projects + projects, error_message = project_client.api_request_handler.project_handler.get_projects( + is_completed=is_completed, limit=limit, offset=offset + ) + + if error_message: + environment.elog(f"Error: Failed to retrieve projects: {error_message}") + raise SystemExit(1) + + if json_output: + print(json.dumps(projects, indent=2)) + return + + # Display results + environment.log(f"Found {len(projects)} project(s).") + environment.log("") + + for project in projects: + display_project(environment, project, show_all_fields) + environment.log("") + + environment.log("Project listing completed successfully.") diff --git a/trcli/commands/cmd_templates.py b/trcli/commands/cmd_templates.py new file mode 100644 index 0000000..af85404 --- /dev/null +++ b/trcli/commands/cmd_templates.py @@ -0,0 +1,77 @@ +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"Templates {action} Execution Parameters" f"\n> TestRail instance: {env.host} (user: {env.username})") + + +def display_template(env: Environment, template: dict): + """Helper function to display a single template's information.""" + env.log(f"Template ID: {template.get('id')}") + env.log(f" Name: {template.get('name', 'N/A')}") + env.log(f" Default: {'Yes' if template.get('is_default') else 'No'}") + env.log(f" Custom ID: {template.get('i18n_custom_id', 'N/A')}") + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.pass_context +@pass_environment +def cli(environment: Environment, context: click.Context, *args, **kwargs): + """Manage templates in TestRail""" + environment.cmd = "templates" + environment.set_parameters(context) + + +@cli.command() +@click.option( + "--project-id", type=click.IntRange(min=1), metavar="", required=True, help="Get templates for project ID." +) +@click.option("--json-output", is_flag=True, help="Output templates as raw JSON from API.") +@click.pass_context +@pass_environment +def list( + environment: Environment, + context: click.Context, + project_id: int, + json_output: bool, + *args, + **kwargs, +): + """List all templates (field layouts) for a project""" + 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), + ) + + # Retrieve templates + environment.log(f"Retrieving templates for project ID {project_id}...") + templates, error_message = project_client.api_request_handler.template_handler.get_templates(project_id) + + if error_message: + environment.elog(f"Error: Failed to retrieve templates: {error_message}") + raise SystemExit(1) + + if json_output: + print(json.dumps(templates, indent=2)) + return + + # Display results + environment.log(f"Found {len(templates)} template(s).") + environment.log("") + + for template in templates: + display_template(environment, template) + environment.log("") + + environment.log("Template 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..ff055e7 100644 --- a/trcli/constants.py +++ b/trcli/constants.py @@ -100,6 +100,11 @@ statuses=dict(**FAULT_MAPPING), casefields=dict(**FAULT_MAPPING), resultfields=dict(**FAULT_MAPPING), + priorities=dict(**FAULT_MAPPING), + casetypes=dict(**FAULT_MAPPING), + users=dict(**FAULT_MAPPING), + projects=dict(**FAULT_MAPPING), + templates=dict(**FAULT_MAPPING), ) PROMPT_MESSAGES = dict( @@ -136,6 +141,11 @@ - 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) + - projects: Query projects (get and list) + - templates: Query templates (list) - results: Query and update test results (list, update)""" MISSING_COMMAND_SLOGAN = """Usage: trcli [OPTIONS] COMMAND [ARGS]...\nTry 'trcli --help' for help.