diff --git a/README.md b/README.md index ab8800cb..eae4c37a 100644 --- a/README.md +++ b/README.md @@ -289,13 +289,14 @@ will be used to upload all results into the same test run. #### Labels Management -The TestRail CLI provides comprehensive label management capabilities using the `labels` command. Labels help categorize and organize your test management assets efficiently, making it easier to filter and manage test cases, runs, and projects. +The TestRail CLI provides comprehensive label management capabilities for **Projects** using the `labels` command. Labels help categorize and organize your test management assets efficiently, making it easier to filter and manage test cases, runs, and projects. -The TestRail CLI supports two types of label management: -- **Project Labels**: Manage labels at the project level -- **Test Case Labels**: Apply labels to specific test cases for better organization and filtering - -Both types of labels support full CRUD (Create, Read, Update, Delete) operations with comprehensive validation and error handling. +The `labels` command supports full CRUD (Create, Read, Update, Delete) operations: +- **Add** new labels to projects +- **List** existing labels with pagination support +- **Get** detailed information about specific labels +- **Update** existing label titles +- **Delete** single or multiple labels in batch ##### Reference ```shell @@ -309,25 +310,13 @@ Options: Commands: add Add a new label in TestRail - cases Manage labels for test cases delete Delete labels from TestRail get Get a specific label by ID list List all labels in the project update Update an existing label in TestRail ``` -#### Project Labels - -Project labels are managed using the main `labels` command and provide project-wide label management capabilities. These labels can be created, updated, deleted, and listed at the project level. - -**Project Labels Support:** -- **Add** new labels to projects -- **List** existing labels with pagination support -- **Get** detailed information about specific labels -- **Update** existing label titles -- **Delete** single or multiple labels in batch - -###### Adding Labels +##### Adding Labels Create new labels for your project with a descriptive title (maximum 20 characters). ```shell @@ -347,7 +336,7 @@ $ trcli -h https://yourinstance.testrail.io --username --passwor labels add --title "Regression" ``` -###### Listing Labels +##### Listing Labels View all labels in your project with optional pagination support. ```shell @@ -378,7 +367,7 @@ Found 5 labels: ID: 127, Title: 'Performance' ``` -###### Getting Label Details +##### Getting Label Details Retrieve detailed information about a specific label by its ID. ```shell @@ -398,7 +387,7 @@ Label details: Created on: 1234567890 ``` -###### Updating Labels +##### Updating Labels Modify the title of existing labels (maximum 20 characters). ```shell @@ -414,7 +403,7 @@ Updating label with ID 123... Successfully updated label: ID=123, Title='High-Priority' ``` -###### Deleting Labels +##### Deleting Labels Remove single or multiple labels from your project. ```shell @@ -437,7 +426,7 @@ Deleting labels with IDs: 123,124... Successfully deleted 2 label(s) ``` -###### Common Use Cases +##### Common Use Cases **1. Release Management** ```shell @@ -492,7 +481,7 @@ $ trcli -h https://yourinstance.testrail.io --username --passwor labels delete --ids "100,101,102,103,104" ``` -###### Command Options Reference +##### Command Options Reference **Add Command:** ```shell @@ -536,7 +525,7 @@ Options: --help Show this message and exit. ``` -###### Error Handling and Validation +##### Error Handling and Validation The labels command includes comprehensive validation: @@ -560,192 +549,6 @@ $ trcli labels delete --ids "abc,def" Error: Invalid label IDs format ``` -#### Test Case Labels - -In addition to project-level labels, the TestRail CLI also supports **test case label management** through the `labels cases` command. This functionality allows you to assign labels to specific test cases and filter test cases by their labels, providing powerful organization and filtering capabilities for your test suite. - -###### Test Case Label Features -- **Add labels to test cases**: Apply existing or new labels to one or multiple test cases (supports multiple labels per command) -- **Get labels assigned to test cases**: View all labels currently assigned to specific test cases -- **List test cases by labels**: Find test cases that have specific labels applied -- **Multiple labels support**: Add multiple labels in a single command using comma separation -- **Automatic label creation**: Labels are created automatically if they don't exist when adding to cases -- **Maximum label validation**: Enforces TestRail's limit of 10 labels per test case -- **Flexible filtering**: Search by label ID or title - -###### Reference -```shell -$ trcli labels cases --help -Usage: trcli labels cases [OPTIONS] COMMAND [ARGS]... - - Manage labels for test cases - -Options: - --help Show this message and exit. - -Commands: - add Add a label to test cases - get Get labels assigned to test cases - list List test cases filtered by label ID or title -``` - -###### Adding Labels to Test Cases -Apply labels to one or multiple test cases. If the label doesn't exist, it will be created automatically. - -```shell -# Add a label to a single test case -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases add --case-ids 123 --title "Regression" - -# Add a label to multiple test cases -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases add --case-ids "123,124,125" --title "Critical" - -# Add a release label to test cases -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases add --case-ids "100,101,102" --title "Sprint-42" - -# Add multiple labels to test cases (comma-separated) -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases add --case-ids "123,124" --title "Regression,Critical,UI" - -# Add multiple labels with spaces (properly quoted) -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases add --case-ids "100" --title "Bug Fix, Performance" -``` - -###### Getting Labels Assigned to Test Cases -View all labels assigned to specific test cases. This is useful for inspecting the current label assignments and understanding how test cases are categorized. - -```shell -# Get labels for a single test case -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases get --case-ids 123 - -# Get labels for multiple test cases -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases get --case-ids "123,124,125" - -# Get labels for test cases to audit label usage -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases get --case-ids "100,101,102,103,104" -``` - -**Output example:** -``` -Retrieving labels for 3 test case(s)... -Found 3 test case(s): - - Case ID: 123, Title: 'Login functionality test' [Labels: ID:5,Title:'Regression'; ID:7,Title:'Critical'] - Case ID: 124, Title: 'Password validation test' [Labels: ID:5,Title:'Regression'] - Case ID: 125, Title: 'User registration test' [No labels] -``` - -###### Listing Test Cases by Labels -Find test cases that have specific labels applied, either by label ID or title. Note that `--ids` and `--title` options are mutually exclusive - use only one at a time. - -```shell -# List test cases by label title -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases list --title "Regression" - -# List test cases by label ID -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases list --ids 123 - -# List test cases by multiple label IDs -$ trcli -h https://yourinstance.testrail.io --username --password \ - --project "Your Project" \ - labels cases list --ids "123,124,125" -``` - -**Output example:** -``` -Retrieving test cases with label title 'Regression'... -Found 3 matching test case(s): - - Case ID: 123, Title: 'Login functionality test' [Labels: ID:5,Title:'Regression'; ID:7,Title:'Critical'] - Case ID: 124, Title: 'Password validation test' [Labels: ID:5,Title:'Regression'] - Case ID: 125, Title: 'User registration test' [Labels: ID:5,Title:'Regression'; ID:8,Title:'UI'] -``` - -**No matches example:** -``` -Retrieving test cases with label title 'Non-Existent'... -Found 0 matching test case(s): - No test cases found with label title 'Non-Existent'. -``` - -###### Command Options Reference - -**Add Cases Command:** -```shell -$ trcli labels cases add --help -Options: - --case-ids Comma-separated list of test case IDs [required] - --title Label title(s) to add (max 20 characters each). Use comma separation for multiple labels [required] - --help Show this message and exit. -``` - -**Get Cases Command:** -```shell -$ trcli labels cases get --help -Options: - --case-ids Comma-separated list of test case IDs [required] - --help Show this message and exit. -``` - -**List Cases Command:** -```shell -$ trcli labels cases list --help -Options: - --ids Comma-separated list of label IDs to filter by (mutually exclusive with --title) - --title Label title to filter by (max 20 characters) (mutually exclusive with --ids) - --help Show this message and exit. -``` - -###### Validation Rules - -**Test Case Label Management includes these validations:** - -- **Label Title**: Maximum 20 characters (same as project labels) -- **Case IDs**: Must be valid integers in comma-separated format -- **Maximum Labels**: Each test case can have maximum 10 labels -- **Filter Requirements**: Either `--ids` or `--title` must be provided for list command (mutually exclusive) -- **Label Creation**: Labels are automatically created if they don't exist when adding to cases -- **Duplicate Prevention**: Adding an existing label to a case is handled gracefully - -###### Error Handling Examples - -**Test Case Label Management error scenarios:** -```shell -# Missing both filter options -$ trcli labels cases list -Error: Either --ids or --title must be provided. - -# Using both filter options (mutually exclusive) -$ trcli labels cases list --ids 123 --title "Regression" -Error: --ids and --title options are mutually exclusive. Use only one at a time. - -# Invalid case ID format -$ trcli labels cases add --case-ids "abc,def" --title "Test" -Error: Invalid case IDs format. Use comma-separated integers (e.g., 1,2,3). - -# Invalid label title length in multiple labels -$ trcli labels cases add --case-ids "123" --title "Valid,this-title-is-way-too-long" -Error: Label title 'this-title-is-way-too-long' must be 20 characters or less. -``` - ### Reference ```shell $ trcli add_run --help diff --git a/tests/test_api_request_handler_labels.py b/tests/test_api_request_handler_labels.py index a2ef6a48..60b2206c 100644 --- a/tests/test_api_request_handler_labels.py +++ b/tests/test_api_request_handler_labels.py @@ -1,5 +1,5 @@ import pytest -from unittest.mock import Mock, patch, MagicMock, call +from unittest.mock import Mock, patch, MagicMock from pathlib import Path import json from serde.json import from_json @@ -340,369 +340,4 @@ def test_delete_labels_forbidden(self, labels_handler): success, error = labels_handler.delete_labels(label_ids=[1]) assert success is False - assert error == "No access to the project" - - -class TestApiRequestHandlerLabelsCases: - """Test cases for test case label operations""" - - def setup_method(self): - """Set up test fixtures""" - # Create proper objects like the existing fixture - api_client = APIClient(host_name="http://test.com") - environment = Environment() - environment.project = "Test Project" - environment.batch_size = 10 - - # Create a minimal TestRailSuite for testing - from trcli.data_classes.dataclass_testrail import TestRailSuite - test_suite = TestRailSuite(name="Test Suite") - - self.labels_handler = ApiRequestHandler(environment, api_client, test_suite, verify=False) - - def test_add_labels_to_cases_success(self): - """Test successful addition of labels to test cases""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases, \ - patch.object(self.labels_handler, 'get_labels') as mock_get_labels, \ - patch.object(self.labels_handler, 'add_label') as mock_add_label, \ - patch.object(self.labels_handler.client, 'send_get') as mock_send_get, \ - patch.object(self.labels_handler.client, 'send_post') as mock_send_post: - - # Mock __get_all_cases response (cases exist) - mock_get_cases.return_value = ([ - {"id": 1, "title": "Case 1", "suite_id": 1}, - {"id": 2, "title": "Case 2", "suite_id": 1} - ], "") - - # Mock get_labels response (label doesn't exist) - mock_get_labels.return_value = ({"labels": []}, "") - - # Mock add_label response (create new label) - mock_add_label.return_value = ({"label": {"id": 5, "title": "test-label"}}, "") - - # Mock get_case responses - mock_send_get.side_effect = [ - MagicMock(status_code=200, response_text={"labels": [], "suite_id": 1, "title": "Case 1"}), # Case 1 - MagicMock(status_code=200, response_text={"labels": [], "suite_id": 1, "title": "Case 2"}) # Case 2 - ] - - # Mock update_cases batch response (for multiple cases) - mock_send_post.return_value = MagicMock(status_code=200) - - # Test the method - results, error_message = self.labels_handler.add_labels_to_cases( - case_ids=[1, 2], - titles="test-label", - project_id=1 - ) - - # Verify no error - assert error_message == "" - - # Verify results - assert len(results['successful_cases']) == 2 - assert len(results['failed_cases']) == 0 - assert len(results['max_labels_reached']) == 0 - assert len(results['case_not_found']) == 0 - - # Verify API calls - should be called twice: once for multi-suite detection, once for case validation - assert mock_get_cases.call_count == 2 - mock_get_cases.assert_has_calls([ - call(1, None), # Multi-suite detection - call(1, None) # Case validation - ]) - mock_get_labels.assert_called_once_with(1) - mock_add_label.assert_called_once_with(1, "test-label") - assert mock_send_get.call_count == 2 - # Should call update_case/{case_id} twice for individual updates - expected_calls = [ - call("update_case/1", payload={'labels': [5]}), - call("update_case/2", payload={'labels': [5]}) - ] - mock_send_post.assert_has_calls(expected_calls, any_order=False) - - def test_add_labels_to_cases_single_case(self): - """Test adding labels to a single test case using update_case endpoint""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases, \ - patch.object(self.labels_handler, 'get_labels') as mock_get_labels, \ - patch.object(self.labels_handler, 'add_label') as mock_add_label, \ - patch.object(self.labels_handler.client, 'send_get') as mock_send_get, \ - patch.object(self.labels_handler.client, 'send_post') as mock_send_post: - - # Mock __get_all_cases response (case exists) - mock_get_cases.return_value = ([ - {"id": 1, "title": "Case 1"} - ], "") - - # Mock get_labels response (label doesn't exist) - mock_get_labels.return_value = ({"labels": []}, "") - - # Mock add_label response (create new label) - mock_add_label.return_value = ({"label": {"id": 5, "title": "test-label"}}, "") - - # Mock get_case response - mock_send_get.return_value = MagicMock( - status_code=200, - response_text={"labels": [], "suite_id": 1, "title": "Case 1"} - ) - - # Mock update_case response (for single case) - mock_send_post.return_value = MagicMock(status_code=200) - - # Test the method with single case - results, error_message = self.labels_handler.add_labels_to_cases( - case_ids=[1], - titles="test-label", - project_id=1 - ) - - # Verify no error - assert error_message == "" - - # Verify results - assert len(results['successful_cases']) == 1 - assert len(results['failed_cases']) == 0 - assert len(results['max_labels_reached']) == 0 - assert len(results['case_not_found']) == 0 - - # Verify API calls - assert mock_get_cases.call_count == 2 - mock_get_labels.assert_called_once_with(1) - mock_add_label.assert_called_once_with(1, "test-label") - assert mock_send_get.call_count == 1 - # Should call update_case/{case_id} once for single case - mock_send_post.assert_called_once_with("update_case/1", payload={'labels': [5]}) - - def test_add_labels_to_cases_existing_label(self): - """Test adding labels when label already exists""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases, \ - patch.object(self.labels_handler, 'get_labels') as mock_get_labels, \ - patch.object(self.labels_handler, 'add_label') as mock_add_label, \ - patch.object(self.labels_handler.client, 'send_get') as mock_send_get, \ - patch.object(self.labels_handler.client, 'send_post') as mock_send_post: - - # Mock __get_all_cases response (case exists) - mock_get_cases.return_value = ([{"id": 1, "title": "Case 1"}], "") - - # Mock get_labels response (label exists) - mock_get_labels.return_value = ({"labels": [{"id": 5, "title": "test-label"}]}, "") - - # Mock get_case response - mock_send_get.return_value = MagicMock(status_code=200, response_text={"labels": [], "section_id": 1, "title": "Case 1"}) - - # Mock add_label_to_case response - mock_send_post.return_value = MagicMock(status_code=200) - - # Test the method - results, error_message = self.labels_handler.add_labels_to_cases( - case_ids=[1], - titles="test-label", - project_id=1 - ) - - # Verify no error - assert error_message == "" - - # Verify results - assert len(results['successful_cases']) == 1 - assert len(results['case_not_found']) == 0 - - # Verify add_label was not called (label already exists) - mock_add_label.assert_not_called() - - def test_add_labels_to_cases_max_labels_reached(self): - """Test handling of maximum labels limit (10)""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases, \ - patch.object(self.labels_handler, 'get_labels') as mock_get_labels, \ - patch.object(self.labels_handler.client, 'send_get') as mock_send_get: - - # Mock __get_all_cases response (case exists) - mock_get_cases.return_value = ([{"id": 1, "title": "Case 1"}], "") - - # Mock get_labels response - mock_get_labels.return_value = ({"labels": [{"id": 15, "title": "test-label"}]}, "") - - # Mock get_case response with 10 existing labels (different from test-label) - existing_labels = [{"id": i, "title": f"label-{i}"} for i in range(1, 11)] - mock_send_get.return_value = MagicMock( - status_code=200, - response_text={"labels": existing_labels} - ) - - # Test the method - results, error_message = self.labels_handler.add_labels_to_cases( - case_ids=[1], - titles="test-label", - project_id=1 - ) - - # Verify no error - assert error_message == "" - - # Verify results - assert len(results['successful_cases']) == 0 - assert len(results['failed_cases']) == 0 - assert len(results['max_labels_reached']) == 1 - assert len(results['case_not_found']) == 0 - assert results['max_labels_reached'][0] == 1 - - def test_add_labels_to_cases_label_already_on_case(self): - """Test handling when label already exists on case""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases, \ - patch.object(self.labels_handler, 'get_labels') as mock_get_labels, \ - patch.object(self.labels_handler.client, 'send_get') as mock_send_get: - - # Mock __get_all_cases response (case exists) - mock_get_cases.return_value = ([{"id": 1, "title": "Case 1"}], "") - - # Mock get_labels response - mock_get_labels.return_value = ({"labels": [{"id": 5, "title": "test-label"}]}, "") - - # Mock get_case response with the label already present - mock_send_get.return_value = MagicMock( - status_code=200, - response_text={"labels": [{"id": 5, "title": "test-label"}]} - ) - - # Test the method - results, error_message = self.labels_handler.add_labels_to_cases( - case_ids=[1], - titles="test-label", - project_id=1 - ) - - # Verify no error - assert error_message == "" - - # Verify results - assert len(results['successful_cases']) == 1 - assert len(results['case_not_found']) == 0 - assert "already exist" in results['successful_cases'][0]['message'] - - def test_add_labels_to_cases_case_not_found(self): - """Test handling when case IDs don't exist""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases: - - # Mock __get_all_cases response (no cases exist) - mock_get_cases.return_value = ([], "") - - # Test the method with case IDs that don't exist - results, error_message = self.labels_handler.add_labels_to_cases( - case_ids=[999, 1000, 1001], - titles="test-label", - project_id=1 - ) - - # Verify no error - assert error_message == "" - - # Verify results - all cases should be in case_not_found - assert len(results['case_not_found']) == 3 - assert 999 in results['case_not_found'] - assert 1000 in results['case_not_found'] - assert 1001 in results['case_not_found'] - - # Verify that no other processing happened since no valid cases - assert len(results['successful_cases']) == 0 - assert len(results['failed_cases']) == 0 - assert len(results['max_labels_reached']) == 0 - - def test_get_cases_by_label_with_label_ids(self): - """Test getting cases by label IDs""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases: - - # Mock cases response - mock_cases = [ - {"id": 1, "title": "Test Case 1", "labels": [{"id": 5, "title": "label1"}]}, - {"id": 2, "title": "Test Case 2", "labels": [{"id": 6, "title": "label2"}]}, - {"id": 3, "title": "Test Case 3", "labels": [{"id": 5, "title": "label1"}]} - ] - mock_get_cases.return_value = (mock_cases, "") - - # Test the method - matching_cases, error_message = self.labels_handler.get_cases_by_label( - project_id=1, - suite_id=None, - label_ids=[5] - ) - - # Verify no error - assert error_message == "" - - # Verify results (should return cases 1 and 3) - assert len(matching_cases) == 2 - assert matching_cases[0]['id'] == 1 - assert matching_cases[1]['id'] == 3 - - def test_get_cases_by_label_with_title(self): - """Test getting cases by label title""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases, \ - patch.object(self.labels_handler, 'get_labels') as mock_get_labels: - - # Mock labels response - mock_get_labels.return_value = ({"labels": [{"id": 5, "title": "test-label"}]}, "") - - # Mock cases response - mock_cases = [ - {"id": 1, "title": "Test Case 1", "labels": [{"id": 5, "title": "test-label"}]}, - {"id": 2, "title": "Test Case 2", "labels": [{"id": 6, "title": "other-label"}]} - ] - mock_get_cases.return_value = (mock_cases, "") - - # Test the method - matching_cases, error_message = self.labels_handler.get_cases_by_label( - project_id=1, - suite_id=None, - label_title="test-label" - ) - - # Verify no error - assert error_message == "" - - # Verify results (should return case 1) - assert len(matching_cases) == 1 - assert matching_cases[0]['id'] == 1 - - def test_get_cases_by_label_title_not_found(self): - """Test getting cases by non-existent label title""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases, \ - patch.object(self.labels_handler, 'get_labels') as mock_get_labels: - - # Mock labels response (no matching label) - mock_get_labels.return_value = ({"labels": []}, "") - - # Mock get_all_cases to return empty (not called due to early return) - mock_get_cases.return_value = ([], "") - - # Test the method - matching_cases, error_message = self.labels_handler.get_cases_by_label( - project_id=1, - suite_id=None, - label_title="non-existent-label" - ) - - # Verify error - assert error_message == "" - assert matching_cases == [] - - def test_get_cases_by_label_no_matching_cases(self): - """Test getting cases when no cases have the specified label""" - with patch.object(self.labels_handler, '_ApiRequestHandler__get_all_cases') as mock_get_cases: - - # Mock cases response (no cases with target label) - mock_cases = [ - {"id": 1, "title": "Test Case 1", "labels": [{"id": 6, "title": "other-label"}]}, - {"id": 2, "title": "Test Case 2", "labels": []} - ] - mock_get_cases.return_value = (mock_cases, "") - - # Test the method - matching_cases, error_message = self.labels_handler.get_cases_by_label( - project_id=1, - suite_id=None, - label_ids=[5] - ) - - # Verify no error but no results - assert error_message == "" - assert len(matching_cases) == 0 \ No newline at end of file + assert error == "No access to the project" \ No newline at end of file diff --git a/tests/test_cmd_labels.py b/tests/test_cmd_labels.py index 9158d6c0..18a1a0e2 100644 --- a/tests/test_cmd_labels.py +++ b/tests/test_cmd_labels.py @@ -345,597 +345,4 @@ def test_print_config(self): "\n> TestRail instance: https://test.testrail.com (user: test@example.com)" "\n> Project: Test Project" ) - mock_log.assert_called_once_with(expected_message) - - -class TestLabelsCasesCommands: - """Test cases for test case label CLI commands""" - - def setup_method(self): - """Set up test fixtures""" - self.runner = CliRunner() - self.environment = Environment() - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_labels_to_cases_success(self, mock_project_client): - """Test successful addition of labels to test cases""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.suite.suite_id = None - mock_client_instance.api_request_handler.add_labels_to_cases.return_value = ( - { - 'successful_cases': [ - {'case_id': 1, 'message': "Successfully added label 'test-label' to case 1"}, - {'case_id': 2, 'message': "Successfully added label 'test-label' to case 2"} - ], - 'failed_cases': [], - 'max_labels_reached': [], - 'case_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'): - - result = self.runner.invoke( - cmd_labels.cases, - ['add', '--case-ids', '1,2', '--title', 'test-label'], - obj=self.environment - ) - - assert result.exit_code == 0 - mock_client_instance.api_request_handler.add_labels_to_cases.assert_called_once_with( - case_ids=[1, 2], - titles=['test-label'], - project_id=1, - suite_id=None - ) - - # Verify success messages were logged - mock_log.assert_any_call("Successfully processed 2 case(s):") - mock_log.assert_any_call(" Case 1: Successfully added label 'test-label' to case 1") - mock_log.assert_any_call(" Case 2: Successfully added label 'test-label' to case 2") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_multiple_labels_to_cases_success(self, mock_project_client): - """Test successful addition of multiple labels to test cases""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.suite.suite_id = None - mock_client_instance.api_request_handler.add_labels_to_cases.return_value = ( - { - 'successful_cases': [ - {'case_id': 1, 'message': "Successfully added 2 labels to case 1"}, - {'case_id': 2, 'message': "Successfully added 2 labels to case 2"} - ], - 'failed_cases': [], - 'max_labels_reached': [], - 'case_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'): - - result = self.runner.invoke( - cmd_labels.cases, - ['add', '--case-ids', '1,2', '--title', 'label1, label2'], - obj=self.environment - ) - - assert result.exit_code == 0 - mock_client_instance.api_request_handler.add_labels_to_cases.assert_called_once_with( - case_ids=[1, 2], - titles=['label1', 'label2'], - project_id=1, - suite_id=None - ) - - # Verify success messages were logged - mock_log.assert_any_call("Successfully processed 2 case(s):") - mock_log.assert_any_call(" Case 1: Successfully added 2 labels to case 1") - mock_log.assert_any_call(" Case 2: Successfully added 2 labels to case 2") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_labels_to_cases_with_max_labels_reached(self, mock_project_client): - """Test addition of labels with some cases reaching maximum labels""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.api_request_handler.add_labels_to_cases.return_value = ( - { - 'successful_cases': [ - {'case_id': 1, 'message': "Successfully added label 'test-label' to case 1"} - ], - 'failed_cases': [], - 'max_labels_reached': [2, 3], - 'case_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'): - - result = self.runner.invoke( - cmd_labels.cases, - ['add', '--case-ids', '1,2,3', '--title', 'test-label'], - obj=self.environment - ) - - assert result.exit_code == 0 - - # Verify warning messages were logged - mock_log.assert_any_call("Warning: 2 case(s) already have maximum labels (10):") - mock_log.assert_any_call(" Case 2: Maximum labels reached") - mock_log.assert_any_call(" Case 3: Maximum labels reached") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_labels_to_cases_title_too_long(self, mock_project_client): - """Test title length validation - should fail when all labels are invalid""" - 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_labels.cases, - ['add', '--case-ids', '1', '--title', 'this-title-is-way-too-long-for-testrail'], - obj=self.environment - ) - - assert result.exit_code == 1 - # Should show warning for invalid label, then error for no valid labels - mock_elog.assert_any_call("Warning: Label title 'this-title-is-way-too-long-for-testrail' exceeds 20 character limit and will be skipped.") - mock_elog.assert_any_call("Error: No valid label titles provided after filtering.") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_labels_max_labels_validation(self, mock_project_client): - """Test early validation for more than 10 labels""" - with patch.object(self.environment, 'elog') as mock_elog, \ - patch.object(self.environment, 'set_parameters'), \ - patch.object(self.environment, 'check_for_required_parameters'): - - # Create a title string with 11 labels - long_title_list = ','.join([f'label{i}' for i in range(1, 12)]) - - result = self.runner.invoke( - cmd_labels.cases, - ['add', '--case-ids', '1', '--title', long_title_list], - obj=self.environment - ) - - assert result.exit_code == 1 - mock_elog.assert_called_with("Error: Cannot add more than 10 labels at once. You provided 11 valid labels.") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_single_label_success_message(self, mock_project_client): - """Test that success message shows the correct label that was actually added""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.suite.suite_id = None - mock_client_instance.api_request_handler.add_labels_to_cases.return_value = ( - { - 'successful_cases': [ - {'case_id': 1, 'message': "Successfully added label 'newlabel' to case 1"} - ], - 'failed_cases': [], - 'max_labels_reached': [], - 'case_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'): - - result = self.runner.invoke( - cmd_labels.cases, - ['add', '--case-ids', '1', '--title', 'newlabel'], - obj=self.environment - ) - - assert result.exit_code == 0 - mock_client_instance.api_request_handler.add_labels_to_cases.assert_called_once_with( - case_ids=[1], - titles=['newlabel'], - project_id=1, - suite_id=None - ) - - # Verify the correct success message - mock_log.assert_any_call("Successfully processed 1 case(s):") - mock_log.assert_any_call(" Case 1: Successfully added label 'newlabel' to case 1") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_multiple_labels_mixed_valid_invalid(self, mock_project_client): - """Test mixed valid/invalid labels - should process valid ones and warn about invalid ones""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.suite.suite_id = None - mock_client_instance.api_request_handler.add_labels_to_cases.return_value = ( - { - 'successful_cases': [ - {'case_id': 1, 'message': "Successfully added label 'valid-label' to case 1"} - ], - 'failed_cases': [], - 'max_labels_reached': [], - 'case_not_found': [] - }, - "" - ) - - with patch.object(self.environment, 'log') as mock_log, \ - 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_labels.cases, - ['add', '--case-ids', '1', '--title', 'valid-label,this-title-is-way-too-long-for-testrail'], - obj=self.environment - ) - - # Should succeed with valid label - assert result.exit_code == 0 - - # Should warn about invalid label - mock_elog.assert_any_call("Warning: Label title 'this-title-is-way-too-long-for-testrail' exceeds 20 character limit and will be skipped.") - - # Should process the valid label - mock_client_instance.api_request_handler.add_labels_to_cases.assert_called_once_with( - case_ids=[1], - titles=['valid-label'], - project_id=1, - suite_id=None - ) - - # Should show success for valid label - mock_log.assert_any_call("Successfully processed 1 case(s):") - mock_log.assert_any_call(" Case 1: Successfully added label 'valid-label' to case 1") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_labels_all_invalid_titles(self, mock_project_client): - """Test when all labels are invalid - should fail""" - 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_labels.cases, - ['add', '--case-ids', '1', '--title', 'this-title-is-way-too-long,another-title-that-is-also-too-long'], - obj=self.environment - ) - - # Should fail when all labels are invalid - assert result.exit_code == 1 - - # Should show warnings for all invalid labels - mock_elog.assert_any_call("Warning: Label title 'this-title-is-way-too-long' exceeds 20 character limit and will be skipped.") - mock_elog.assert_any_call("Warning: Label title 'another-title-that-is-also-too-long' exceeds 20 character limit and will be skipped.") - mock_elog.assert_any_call("Error: No valid label titles provided after filtering.") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_labels_to_cases_invalid_case_ids(self, mock_project_client): - """Test invalid case IDs format""" - 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_labels.cases, - ['add', '--case-ids', 'invalid,ids', '--title', 'test-label'], - obj=self.environment - ) - - assert result.exit_code == 1 - mock_elog.assert_called_with("Error: Invalid case IDs format. Use comma-separated integers (e.g., 1,2,3).") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_add_labels_to_cases_case_not_found(self, mock_project_client): - """Test handling of non-existent case IDs""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.api_request_handler.add_labels_to_cases.return_value = ( - { - 'successful_cases': [ - {'case_id': 1, 'message': "Successfully added label 'test-label' to case 1"} - ], - 'failed_cases': [], - 'max_labels_reached': [], - 'case_not_found': [999, 1000] - }, - "" - ) - - with patch.object(self.environment, 'log') as mock_log, \ - 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_labels.cases, - ['add', '--case-ids', '1,999,1000', '--title', 'test-label'], - obj=self.environment - ) - - assert result.exit_code == 1 - - # Verify error messages were logged - mock_elog.assert_any_call("Error: 2 test case(s) not found:") - mock_elog.assert_any_call(" Case ID 999 does not exist in the project") - mock_elog.assert_any_call(" Case ID 1000 does not exist in the project") - - # Verify success messages were still logged - mock_log.assert_any_call("Successfully processed 1 case(s):") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_list_cases_by_label_ids_success(self, mock_project_client): - """Test successful listing of cases by label IDs""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.suite = None - mock_client_instance.api_request_handler.get_cases_by_label.return_value = ( - [ - { - 'id': 1, - 'title': 'Test Case 1', - 'labels': [{'id': 5, 'title': 'test-label'}] - }, - { - 'id': 2, - 'title': 'Test Case 2', - 'labels': [{'id': 5, 'title': 'test-label'}, {'id': 6, 'title': 'other-label'}] - } - ], - "" - ) - - 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_labels.cases, - ['list', '--ids', '5'], - obj=self.environment - ) - - assert result.exit_code == 0 - mock_client_instance.api_request_handler.get_cases_by_label.assert_called_once_with( - project_id=1, - suite_id=None, - label_ids=[5], - label_title=None - ) - - # Verify cases were logged - mock_log.assert_any_call("Found 2 matching test case(s):") - mock_log.assert_any_call(" Case ID: 1, Title: 'Test Case 1' [Labels: ID:5,Title:'test-label']") - mock_log.assert_any_call(" Case ID: 2, Title: 'Test Case 2' [Labels: ID:5,Title:'test-label'; ID:6,Title:'other-label']") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_list_cases_by_label_title_success(self, mock_project_client): - """Test successful listing of cases by label title""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.suite = None - mock_client_instance.api_request_handler.get_cases_by_label.return_value = ( - [ - { - 'id': 1, - 'title': 'Test Case 1', - 'labels': [{'id': 5, 'title': 'test-label'}] - } - ], - "" - ) - - 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_labels.cases, - ['list', '--title', 'test-label'], - obj=self.environment - ) - - assert result.exit_code == 0 - mock_client_instance.api_request_handler.get_cases_by_label.assert_called_once_with( - project_id=1, - suite_id=None, - label_ids=None, - label_title='test-label' - ) - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_list_cases_no_filter_provided(self, mock_project_client): - """Test error when neither ids nor title is provided""" - 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_labels.cases, - ['list'], - obj=self.environment - ) - - assert result.exit_code == 1 - mock_elog.assert_called_with("Error: Either --ids or --title must be provided.") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_list_cases_both_filters_provided(self, mock_project_client): - """Test error when both ids and title are provided (mutually exclusive)""" - 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_labels.cases, - ['list', '--ids', '123', '--title', 'test-label'], - obj=self.environment - ) - - assert result.exit_code == 1 - mock_elog.assert_called_with("Error: --ids and --title options are mutually exclusive. Use only one at a time.") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_list_cases_no_matching_cases(self, mock_project_client): - """Test listing when no cases match the label""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.suite = None - mock_client_instance.api_request_handler.get_cases_by_label.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_labels.cases, - ['list', '--title', 'non-existent'], - obj=self.environment - ) - - assert result.exit_code == 0 - mock_log.assert_any_call("Found 0 matching test case(s):") - mock_log.assert_any_call(" No test cases found with label title 'non-existent'.") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_get_case_labels_success(self, mock_project_client): - """Test successful retrieval of labels for specific test cases""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.project.project_id = 1 - mock_client_instance.api_request_handler.get_case_labels.return_value = ( - [ - { - 'id': 123, - 'title': 'Test Case 1', - 'labels': [{'id': 5, 'title': 'Regression'}, {'id': 7, 'title': 'Critical'}] - }, - { - 'id': 124, - 'title': 'Test Case 2', - 'labels': [{'id': 5, 'title': 'Regression'}] - }, - { - 'id': 125, - 'title': 'Test Case 3', - 'labels': [] - } - ], - [] - ) - - 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_labels.cases, - ['get', '--case-ids', '123,124,125'], - obj=self.environment - ) - - assert result.exit_code == 0 - mock_client_instance.api_request_handler.get_case_labels.assert_called_once_with([123, 124, 125]) - - # Verify cases were logged with their labels - mock_log.assert_any_call("Found 3 test case(s):") - mock_log.assert_any_call(" Case ID: 123, Title: 'Test Case 1' [Labels: ID:5,Title:'Regression'; ID:7,Title:'Critical']") - mock_log.assert_any_call(" Case ID: 124, Title: 'Test Case 2' [Labels: ID:5,Title:'Regression']") - mock_log.assert_any_call(" Case ID: 125, Title: 'Test Case 3' [No labels]") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_get_case_labels_invalid_case_ids(self, mock_project_client): - """Test invalid case IDs format in get command""" - 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_labels.cases, - ['get', '--case-ids', 'invalid,ids'], - obj=self.environment - ) - - assert result.exit_code == 1 - mock_elog.assert_called_with("Error: Invalid case IDs format. Use comma-separated integers (e.g., 1,2,3).") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_get_case_labels_api_error(self, mock_project_client): - """Test API error in get case labels command""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.api_request_handler.get_case_labels.return_value = ( - [], ["API Error: Case 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_labels.cases, - ['get', '--case-ids', '999'], - obj=self.environment - ) - - assert result.exit_code == 1 - mock_elog.assert_called_with("Failed to retrieve case labels: API Error: Case not found") - - @mock.patch('trcli.commands.cmd_labels.ProjectBasedClient') - def test_get_case_labels_mixed_valid_invalid(self, mock_project_client): - """Test mixed valid and invalid case IDs - should return valid cases and show errors for invalid ones""" - mock_client_instance = MagicMock() - mock_project_client.return_value = mock_client_instance - mock_client_instance.api_request_handler.get_case_labels.return_value = ( - [ - { - 'id': 123, - 'title': 'Valid Test Case', - 'labels': [{'id': 5, 'title': 'Regression'}] - } - ], - ["Could not retrieve case 999: Case not found", "Could not retrieve case 1000: Case not found"] - ) - - with patch.object(self.environment, 'log') as mock_log, \ - 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_labels.cases, - ['get', '--case-ids', '123,999,1000'], - obj=self.environment - ) - - # Should not exit with error since some cases were successful - assert result.exit_code == 0 - - # Should show success for valid case - mock_log.assert_any_call("Found 1 test case(s):") - mock_log.assert_any_call(" Case ID: 123, Title: 'Valid Test Case' [Labels: ID:5,Title:'Regression']") - - # Should show errors for invalid cases - mock_elog.assert_any_call("Failed to retrieve case labels: Could not retrieve case 999: Case not found") - mock_elog.assert_any_call("Failed to retrieve case labels: Could not retrieve case 1000: Case not found") - - \ No newline at end of file + mock_log.assert_called_once_with(expected_message) \ No newline at end of file diff --git a/tests_e2e/reports_junit/generic_ids_name.xml b/tests_e2e/reports_junit/generic_ids_name.xml index 610de13c..0e9f57d9 100644 --- a/tests_e2e/reports_junit/generic_ids_name.xml +++ b/tests_e2e/reports_junit/generic_ids_name.xml @@ -4,8 +4,8 @@ - - + + failed due to... @@ -14,7 +14,7 @@ - + diff --git a/tests_e2e/reports_junit/generic_ids_property.xml b/tests_e2e/reports_junit/generic_ids_property.xml index a0a947ac..979c959e 100644 --- a/tests_e2e/reports_junit/generic_ids_property.xml +++ b/tests_e2e/reports_junit/generic_ids_property.xml @@ -6,13 +6,13 @@ - + failed due to... - + @@ -21,7 +21,7 @@ - + diff --git a/tests_e2e/test_end2end.py b/tests_e2e/test_end2end.py index afafc4f9..f3e5b187 100644 --- a/tests_e2e/test_end2end.py +++ b/tests_e2e/test_end2end.py @@ -4,11 +4,6 @@ import pytest -def _has_testrail_credentials(): - """Check if TestRail credentials are available in environment variables""" - return bool(os.environ.get("TR_CLI_USERNAME") and os.environ.get("TR_CLI_PASSWORD")) - - def _run_cmd(multiline_cmd: str): lines_list = [] for line in multiline_cmd.splitlines(): @@ -69,8 +64,8 @@ class TestsEndToEnd: # TestRail 101 instance has the required configuration for this test run TR_INSTANCE = "https://testrail101.testrail.io/" # Uncomment and enter your credentials below in order to execute the tests locally - #os.environ.setdefault("TR_CLI_USERNAME", "") - #os.environ.setdefault("TR_CLI_PASSWORD", "") + # os.environ.setdefault("TR_CLI_USERNAME", "") + # os.environ.setdefault("TR_CLI_PASSWORD", "") @pytest.fixture(autouse=True, scope="module") def install_trcli(self): @@ -955,306 +950,4 @@ def test_labels_edge_cases(self): "Successfully deleted 1 label(s)" ] ) - - - def test_labels_cases_full_workflow(self): - """Test complete workflow of test case label operations""" - # First, create a test label - add_label_output = _run_cmd(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels add \\ - --title "e2e-case-label" - """) - _assert_contains( - add_label_output, - [ - "Adding label 'e2e-case-label'...", - "Successfully added label:" - ] - ) - - # Extract label ID for later use - import re - label_id_match = re.search(r"ID=(\d+)", add_label_output) - assert label_id_match, "Could not extract label ID from output" - label_id = label_id_match.group(1) - - try: - # Use known test case IDs that should exist in the test project - # These are typical case IDs that exist in the TestRail test environment - test_case_ids = ["24964", "24965"] # Multiple test cases for batch testing - - # Add labels to test cases (using single-suite project for batch testing) - add_cases_output = _run_cmd(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases add \\ - --case-ids "{','.join(test_case_ids)}" \\ - --title "e2e-case-label" - """) - _assert_contains( - add_cases_output, - [ - f"Adding label 'e2e-case-label' to {len(test_case_ids)} test case(s)...", - "Successfully processed" - ] - ) - - # List test cases by label title (using single-suite project) - list_by_title_output = _run_cmd(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases list \\ - --title "e2e-case-label" - """) - _assert_contains( - list_by_title_output, - [ - "Retrieving test cases with label title 'e2e-case-label'...", - "matching test case(s):" - ] - ) - - # List test cases by label ID (using single-suite project) - list_by_id_output = _run_cmd(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases list \\ - --ids "{label_id}" - """) - _assert_contains( - list_by_id_output, - [ - f"Retrieving test cases with label IDs: {label_id}...", - "matching test case(s):" - ] - ) - - finally: - # Cleanup - delete the test label - delete_output = _run_cmd(f""" -echo "y" | trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels delete \\ - --ids {label_id} - """) - _assert_contains( - delete_output, - [ - f"Deleting labels with IDs: {label_id}...", - "Successfully deleted 1 label(s)" - ] - ) - - def test_labels_cases_validation_errors(self): - """Test validation errors for test case label commands""" - # Test title too long for add cases - long_title_output, return_code = _run_cmd_allow_failure(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases add \\ - --case-ids "1" \\ - --title "this-title-is-way-too-long-for-testrail" - """) - assert return_code != 0 - _assert_contains( - long_title_output, - ["Error: Label title must be 20 characters or less."] - ) - - # Test invalid case IDs format - invalid_ids_output, return_code = _run_cmd_allow_failure(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases add \\ - --case-ids "invalid,ids" \\ - --title "test" - """) - assert return_code != 0 - _assert_contains( - invalid_ids_output, - ["Error: Invalid case IDs format. Use comma-separated integers (e.g., 1,2,3)."] - ) - - # Test missing filter for list cases - no_filter_output, return_code = _run_cmd_allow_failure(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases list - """) - assert return_code != 0 - _assert_contains( - no_filter_output, - ["Error: Either --ids or --title must be provided."] - ) - - # Test title too long for list cases - long_title_list_output, return_code = _run_cmd_allow_failure(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases list \\ - --title "this-title-is-way-too-long-for-testrail" - """) - assert return_code != 0 - _assert_contains( - long_title_list_output, - ["Error: Label title must be 20 characters or less."] - ) - - def test_labels_cases_help_commands(self): - """Test help output for test case label commands""" - # Test main cases help - cases_help_output = _run_cmd("trcli labels cases --help") - _assert_contains( - cases_help_output, - [ - "Usage: trcli labels cases [OPTIONS] COMMAND [ARGS]...", - "Manage labels for test cases", - "add Add a label to test cases", - "list List test cases filtered by label ID or title" - ] - ) - - # Test cases add help - cases_add_help_output = _run_cmd("trcli labels cases add --help") - _assert_contains( - cases_add_help_output, - [ - "Usage: trcli labels cases add [OPTIONS]", - "Add a label to test cases", - "--case-ids", - "--title" - ] - ) - - # Test cases list help - cases_list_help_output = _run_cmd("trcli labels cases list --help") - _assert_contains( - cases_list_help_output, - [ - "Usage: trcli labels cases list [OPTIONS]", - "List test cases filtered by label ID or title", - "--ids", - "--title" - ] - ) - - def test_labels_cases_no_matching_cases(self): - """Test behavior when no test cases match the specified label""" - # Test with non-existent label title - no_match_output = _run_cmd(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases list \\ - --title "non-existent-label" - """) - _assert_contains( - no_match_output, - [ - "Retrieving test cases with label title 'non-existent-label'...", - "Found 0 matching test case(s):", - "No test cases found with label title 'non-existent-label'." - ] - ) - - # Test with non-existent label ID - no_match_id_output = _run_cmd(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases list \\ - --ids "99999" - """) - _assert_contains( - no_match_id_output, - [ - "Retrieving test cases with label IDs: 99999...", - "Found 0 matching test case(s):", - "No test cases found with the specified label IDs." - ] - ) - - def test_labels_cases_single_case_workflow(self): - """Test single case label operations using update_case endpoint""" - # First, create a test label - add_label_output = _run_cmd(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels add \\ - --title "e2e-single-case" - """) - _assert_contains( - add_label_output, - [ - "Adding label 'e2e-single-case'...", - "Successfully added label:" - ] - ) - - # Extract label ID for later use - import re - label_id_match = re.search(r"ID=(\d+)", add_label_output) - assert label_id_match, "Could not extract label ID from output" - label_id = label_id_match.group(1) - - try: - # Use single test case ID for testing update_case endpoint - single_case_id = "24964" - - # Add label to single test case - add_single_case_output = _run_cmd(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases add \\ - --case-ids "{single_case_id}" \\ - --title "e2e-single-case" - """) - _assert_contains( - add_single_case_output, - [ - f"Adding label 'e2e-single-case' to 1 test case(s)...", - "Successfully processed 1 case(s):", - f"Successfully added label 'e2e-single-case' to case {single_case_id}" - ] - ) - - # Verify the label was added by listing cases with this label - list_cases_output = _run_cmd(f""" -trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels cases list \\ - --title "e2e-single-case" - """) - _assert_contains( - list_cases_output, - [ - "Retrieving test cases with label title 'e2e-single-case'...", - "Found 1 matching test case(s):", - f"Case ID: {single_case_id}" - ] - ) - - finally: - # Clean up: delete the test label - _run_cmd(f""" -echo "y" | trcli -y \\ - -h {self.TR_INSTANCE} \\ - --project "SA - (DO NOT DELETE) TRCLI-E2E-Tests" \\ - labels delete \\ - --ids {label_id} - """) \ No newline at end of file diff --git a/trcli/api/api_request_handler.py b/trcli/api/api_request_handler.py index b835bef8..461c7d1e 100644 --- a/trcli/api/api_request_handler.py +++ b/trcli/api/api_request_handler.py @@ -684,10 +684,7 @@ def __get_all_cases(self, project_id=None, suite_id=None) -> Tuple[List[dict], s """ Get all cases from all pages """ - if suite_id is None: - return self.__get_all_entities('cases', f"get_cases/{project_id}") - else: - return self.__get_all_entities('cases', f"get_cases/{project_id}&suite_id={suite_id}") + return self.__get_all_entities('cases', f"get_cases/{project_id}&suite_id={suite_id}") def __get_all_sections(self, project_id=None, suite_id=None) -> Tuple[List[dict], str]: """ @@ -818,257 +815,3 @@ def delete_labels(self, label_ids: List[int]) -> Tuple[bool, str]: response = self.client.send_post("delete_labels", payload=None, files=files) success = response.status_code == 200 return success, response.error_message - - def add_labels_to_cases(self, case_ids: List[int], titles: Union[str, List[str]], project_id: int, suite_id: int = None) -> Tuple[dict, str]: - """ - Add one or more labels to multiple test cases - - :param case_ids: List of test case IDs - :param titles: Label title(s) - either a single string or list of strings (each max 20 characters) - :param project_id: Project ID for validation - :param suite_id: Suite ID (optional) - :returns: Tuple with response data and error string - """ - # Normalize titles to a list - if isinstance(titles, str): - title_list = [titles] - else: - title_list = titles - - # At this point, title_list should already be validated by the CLI - # Just ensure we have clean titles - title_list = [title.strip() for title in title_list if title.strip()] - - if not title_list: - return {}, "No valid labels provided" - - # Initialize results structure - results = { - 'successful_cases': [], - 'failed_cases': [], - 'max_labels_reached': [], - 'case_not_found': [] - } - - # Check if project is multi-suite by getting all cases without suite_id - all_cases_no_suite, error_message = self.__get_all_cases(project_id, None) - if error_message: - return results, error_message - - # Check if project has multiple suites - suite_ids = set() - for case in all_cases_no_suite: - if 'suite_id' in case and case['suite_id']: - suite_ids.add(case['suite_id']) - - # If project has multiple suites and no suite_id provided, require it - if len(suite_ids) > 1 and suite_id is None: - return results, "This project is multisuite, suite id is required" - - # Get all cases to validate that the provided case IDs exist - all_cases, error_message = self.__get_all_cases(project_id, suite_id) - if error_message: - return results, error_message - - # Create a set of existing case IDs for quick lookup - existing_case_ids = {case['id'] for case in all_cases} - - # Validate case IDs and separate valid from invalid ones - invalid_case_ids = [case_id for case_id in case_ids if case_id not in existing_case_ids] - valid_case_ids = [case_id for case_id in case_ids if case_id in existing_case_ids] - - # Record invalid case IDs - for case_id in invalid_case_ids: - results['case_not_found'].append(case_id) - - # If no valid case IDs, return early - if not valid_case_ids: - return results, "" - - # Check if labels exist or create them - existing_labels, error_message = self.get_labels(project_id) - if error_message: - return results, error_message - - # Find existing labels and create missing ones - label_ids = [] - label_id_to_title = {} # Map label IDs to their titles - - for title in title_list: - title = title.strip() # Clean whitespace - if not title: # Skip empty titles - continue - - label_id = None - for label in existing_labels.get('labels', []): - if label.get('title') == title: - label_id = label.get('id') - break - - # Create label if it doesn't exist - if label_id is None: - label_data, error_message = self.add_label(project_id, title) - if error_message: - return results, error_message - label_info = label_data.get('label', label_data) - label_id = label_info.get('id') - - if label_id: - label_ids.append(label_id) - label_id_to_title[label_id] = title # Store the mapping - - # Collect case data and validate constraints - cases_to_update = [] - for case_id in valid_case_ids: - # Get current case to check existing labels - case_response = self.client.send_get(f"get_case/{case_id}") - if case_response.status_code != 200: - results['failed_cases'].append({ - 'case_id': case_id, - 'error': f"Could not retrieve case {case_id}: {case_response.error_message}" - }) - continue - - case_data = case_response.response_text - current_labels = case_data.get('labels', []) - current_label_ids = [label.get('id') for label in current_labels if label.get('id')] - - # Find new labels to add (not already on this case) - new_label_ids = [] - already_exists_titles = [] - - for label_id in label_ids: - if label_id not in current_label_ids: - new_label_ids.append(label_id) - else: - # Use the mapping to get the title for this label_id - if label_id in label_id_to_title: - already_exists_titles.append(label_id_to_title[label_id]) - - # If no new labels to add, record as already exists - if not new_label_ids: - results['successful_cases'].append({ - 'case_id': case_id, - 'message': f"All labels already exist on case {case_id}: {', '.join(already_exists_titles)}" - }) - continue - - # Check maximum labels limit (10) - total_labels_after_update = len(current_label_ids) + len(new_label_ids) - if total_labels_after_update > 10: - results['max_labels_reached'].append(case_id) - continue - - # Prepare case for update and track which labels are being added - updated_label_ids = current_label_ids + new_label_ids - - # Get titles for the new labels being added using the mapping - new_label_titles = [] - for label_id in new_label_ids: - if label_id in label_id_to_title: - new_label_titles.append(label_id_to_title[label_id]) - - cases_to_update.append({ - 'case_id': case_id, - 'labels': updated_label_ids, - 'new_labels': new_label_ids, - 'new_label_titles': new_label_titles - }) - - # Update cases individually to preserve existing labels correctly - # Using individual updates ensures each case keeps its own existing labels - for case_info in cases_to_update: - case_update_data = {'labels': case_info['labels']} - update_response = self.client.send_post(f"update_case/{case_info['case_id']}", payload=case_update_data) - - if update_response.status_code == 200: - # Create message based on number of labels added - new_label_titles = case_info.get('new_label_titles', []) - new_label_count = len(new_label_titles) - - if new_label_count == 1: - message = f"Successfully added label '{new_label_titles[0]}' to case {case_info['case_id']}" - elif new_label_count > 1: - message = f"Successfully added {new_label_count} labels ({', '.join(new_label_titles)}) to case {case_info['case_id']}" - else: - message = f"No new labels added to case {case_info['case_id']}" - - results['successful_cases'].append({ - 'case_id': case_info['case_id'], - 'message': message - }) - else: - results['failed_cases'].append({ - 'case_id': case_info['case_id'], - 'error': update_response.error_message - }) - - return results, "" - - def get_cases_by_label(self, project_id: int, suite_id: int = None, label_ids: List[int] = None, label_title: str = None) -> Tuple[List[dict], str]: - """ - Get test cases filtered by label ID or title - - :param project_id: Project ID - :param suite_id: Suite ID (optional) - :param label_ids: List of label IDs to filter by - :param label_title: Label title to filter by - :returns: Tuple with list of matching cases and error string - """ - # Get all cases first - all_cases, error_message = self.__get_all_cases(project_id, suite_id) - if error_message: - return [], error_message - - # If filtering by title, first get the label ID - target_label_ids = label_ids or [] - if label_title and not target_label_ids: - labels_data, error_message = self.get_labels(project_id) - if error_message: - return [], error_message - - for label in labels_data.get('labels', []): - if label.get('title') == label_title: - target_label_ids.append(label.get('id')) - - if not target_label_ids: - return [], "" # No label found is a valid case with 0 results - - # Filter cases that have any of the target labels - matching_cases = [] - for case in all_cases: - case_labels = case.get('labels', []) - case_label_ids = [label.get('id') for label in case_labels] - - # Check if any of the target label IDs are present in this case - if any(label_id in case_label_ids for label_id in target_label_ids): - matching_cases.append(case) - - return matching_cases, "" - - def get_case_labels(self, case_ids: List[int]) -> Tuple[List[dict], List[str]]: - """ - Get labels assigned to specific test cases - - :param case_ids: List of test case IDs - :returns: Tuple with list of successfully retrieved cases and their labels, and list of error messages for failed cases - """ - results = [] - errors = [] - - for case_id in case_ids: - case_response = self.client.send_get(f"get_case/{case_id}") - if case_response.status_code != 200: - errors.append(f"Could not retrieve case {case_id}: {case_response.error_message}") - continue - - case_data = case_response.response_text - case_labels = case_data.get('labels', []) - - results.append({ - 'id': case_data.get('id'), - 'title': case_data.get('title'), - 'labels': case_labels - }) - - return results, errors diff --git a/trcli/commands/cmd_labels.py b/trcli/commands/cmd_labels.py index 5db329a7..81a1a9c0 100644 --- a/trcli/commands/cmd_labels.py +++ b/trcli/commands/cmd_labels.py @@ -222,239 +222,4 @@ def get(environment: Environment, context: click.Context, label_id: int, *args, environment.log(f" Created on: {label_info.get('created_on', 'N/A')}") else: environment.elog(f"Unexpected response format: {label_data}") - exit(1) - - -@cli.group() -@click.pass_context -@pass_environment -def cases(environment: Environment, context: click.Context, *args, **kwargs): - """Manage labels for test cases""" - pass - - -@cases.command(name='add') -@click.option("--case-ids", required=True, metavar="", help="Comma-separated list of test case IDs (e.g., 1,2,3).") -@click.option("--title", required=True, metavar="", help="Label title(s) to add (max 20 characters each). Use comma separation for multiple labels (e.g., 'label1,label2').") -@click.pass_context -@pass_environment -def add_to_cases(environment: Environment, context: click.Context, case_ids: str, title: str, *args, **kwargs): - """Add one or more labels to test cases""" - environment.check_for_required_parameters() - print_config(environment, "Add Cases") - - # Parse comma-separated titles - title_list = [t.strip() for t in title.split(",") if t.strip()] - - # Filter valid and invalid labels - valid_titles = [] - invalid_titles = [] - - for t in title_list: - if len(t) > 20: - invalid_titles.append(t) - else: - valid_titles.append(t) - - # Show warnings for invalid labels but continue with valid ones - if invalid_titles: - for invalid_title in invalid_titles: - environment.elog(f"Warning: Label title '{invalid_title}' exceeds 20 character limit and will be skipped.") - - # Check if we have any valid labels left - if not valid_titles: - environment.elog("Error: No valid label titles provided after filtering.") - exit(1) - - # Validate maximum number of valid labels (TestRail limit is 10 labels per case) - if len(valid_titles) > 10: - environment.elog(f"Error: Cannot add more than 10 labels at once. You provided {len(valid_titles)} valid labels.") - exit(1) - - # Use only valid titles for processing - title_list = valid_titles - - try: - case_id_list = [int(id.strip()) for id in case_ids.split(",")] - except ValueError: - environment.elog("Error: Invalid case IDs format. Use comma-separated integers (e.g., 1,2,3).") - exit(1) - - project_client = ProjectBasedClient( - environment=environment, - suite=TestRailSuite(name=environment.suite_name, suite_id=environment.suite_id), - ) - project_client.resolve_project() - - # Create appropriate log message - if len(title_list) == 1: - environment.log(f"Adding label '{title_list[0]}' to {len(case_id_list)} test case(s)...") - else: - environment.log(f"Adding {len(title_list)} labels ({', '.join(title_list)}) to {len(case_id_list)} test case(s)...") - - results, error_message = project_client.api_request_handler.add_labels_to_cases( - case_ids=case_id_list, - titles=title_list, - project_id=project_client.project.project_id, - suite_id=environment.suite_id - ) - - # Handle validation errors (but don't exit if there are successful cases) - if error_message: - environment.elog(f"Warning: {error_message}") - - # Always process results (even if there were validation errors) - # Report results - successful_cases = results.get('successful_cases', []) - failed_cases = results.get('failed_cases', []) - max_labels_reached = results.get('max_labels_reached', []) - case_not_found = results.get('case_not_found', []) - - if case_not_found: - environment.elog(f"Error: {len(case_not_found)} test case(s) not found:") - for case_id in case_not_found: - environment.elog(f" Case ID {case_id} does not exist in the project") - - if successful_cases: - environment.log(f"Successfully processed {len(successful_cases)} case(s):") - for case_result in successful_cases: - environment.log(f" Case {case_result['case_id']}: {case_result['message']}") - - if max_labels_reached: - environment.log(f"Warning: {len(max_labels_reached)} case(s) already have maximum labels (10):") - for case_id in max_labels_reached: - environment.log(f" Case {case_id}: Maximum labels reached") - - if failed_cases: - environment.log(f"Failed to process {len(failed_cases)} case(s):") - for case_result in failed_cases: - environment.log(f" Case {case_result['case_id']}: {case_result['error']}") - - # Exit with error if there were invalid case IDs - if case_not_found: - exit(1) - - -@cases.command(name='list') -@click.option("--ids", metavar="", help="Comma-separated list of label IDs to filter by (e.g., 1,2,3).") -@click.option("--title", metavar="", help="Label title to filter by (max 20 characters).") -@click.pass_context -@pass_environment -def list_cases(environment: Environment, context: click.Context, ids: str, title: str, *args, **kwargs): - """List test cases filtered by label ID or title""" - environment.check_for_required_parameters() - - # Validate that either ids or title is provided, but not both - if not ids and not title: - environment.elog("Error: Either --ids or --title must be provided.") - exit(1) - - if ids and title: - environment.elog("Error: --ids and --title options are mutually exclusive. Use only one at a time.") - exit(1) - - if title and len(title) > 20: - environment.elog("Error: Label title must be 20 characters or less.") - exit(1) - - print_config(environment, "List Cases by Label") - - label_ids = None - if ids: - try: - label_ids = [int(id.strip()) for id in ids.split(",")] - except ValueError: - environment.elog("Error: Invalid label IDs format. Use comma-separated integers (e.g., 1,2,3).") - exit(1) - - project_client = ProjectBasedClient( - environment=environment, - suite=TestRailSuite(name=environment.suite_name, suite_id=environment.suite_id), - ) - project_client.resolve_project() - - if title: - environment.log(f"Retrieving test cases with label title '{title}'...") - else: - environment.log(f"Retrieving test cases with label IDs: {', '.join(map(str, label_ids))}...") - - matching_cases, error_message = project_client.api_request_handler.get_cases_by_label( - project_id=project_client.project.project_id, - suite_id=environment.suite_id, - label_ids=label_ids, - label_title=title - ) - - if error_message: - environment.elog(f"Failed to retrieve cases: {error_message}") - exit(1) - else: - environment.log(f"Found {len(matching_cases)} matching test case(s):") - environment.log("") - - if matching_cases: - for case in matching_cases: - case_labels = case.get('labels', []) - label_info = [] - for label in case_labels: - label_info.append(f"ID:{label.get('id')},Title:'{label.get('title')}'") - - labels_str = f" [Labels: {'; '.join(label_info)}]" if label_info else " [No labels]" - environment.log(f" Case ID: {case['id']}, Title: '{case['title']}'{labels_str}") - else: - if title: - environment.log(f" No test cases found with label title '{title}'.") - else: - environment.log(f" No test cases found with the specified label IDs.") - - -@cases.command(name='get') -@click.option("--case-ids", required=True, metavar="", help="Comma-separated list of test case IDs (e.g., 1,2,3).") -@click.pass_context -@pass_environment -def get_case_labels(environment: Environment, context: click.Context, case_ids: str, *args, **kwargs): - """Get labels assigned to test cases""" - environment.check_for_required_parameters() - print_config(environment, "Get Case Labels") - - try: - case_id_list = [int(id.strip()) for id in case_ids.split(",")] - except ValueError: - environment.elog("Error: Invalid case IDs format. Use comma-separated integers (e.g., 1,2,3).") - exit(1) - - project_client = ProjectBasedClient( - environment=environment, - suite=TestRailSuite(name=environment.suite_name, suite_id=environment.suite_id), - ) - project_client.resolve_project() - - environment.log(f"Retrieving labels for {len(case_id_list)} test case(s)...") - - cases_with_labels, error_messages = project_client.api_request_handler.get_case_labels(case_id_list) - - # Display errors for failed cases - if error_messages: - for error in error_messages: - environment.elog(f"Failed to retrieve case labels: {error}") - - # Display results for successful cases - if cases_with_labels: - environment.log(f"Found {len(cases_with_labels)} test case(s):") - environment.log("") - - for case in cases_with_labels: - case_labels = case.get('labels', []) - label_info = [] - for label in case_labels: - label_info.append(f"ID:{label.get('id')},Title:'{label.get('title')}'") - - labels_str = f" [Labels: {'; '.join(label_info)}]" if label_info else " [No labels]" - environment.log(f" Case ID: {case['id']}, Title: '{case['title']}'{labels_str}") - else: - if not error_messages: - environment.log("No test cases found.") - - # Only exit with error if all cases failed - if error_messages and not cases_with_labels: - exit(1) \ No newline at end of file + exit(1) \ No newline at end of file