Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ This project adheres to [Semantic Versioning](https://semver.org/). Version numb
- **MINOR**: New features that are backward-compatible.
- **PATCH**: Bug fixes or minor changes that do not affect backward compatibility.

## [1.14.1]

_released 04-16-2026

### Added
- Keep Markdown Syntax when sending markdown data to Testrail with new WYSIWYG editor - **Requires TestRail 10.3.0 or higher**

### Fixed
- Fixed an issue where attachments are all uploaded to the last result when multiple results are added to case while parsing several reports via glob support

## [1.14.0]

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ trcli
```
You should get something like this:
```
TestRail CLI v1.14.0
TestRail CLI v1.14.1
Copyright 2025 Gurock Software GmbH - www.gurock.com
Supported and loaded modules:
- parse_junit: JUnit XML Files (& Similar)
Expand All @@ -51,7 +51,7 @@ CLI general reference
--------
```shell
$ trcli --help
TestRail CLI v1.14.0
TestRail CLI v1.14.1
Copyright 2025 Gurock Software GmbH - www.gurock.com
Usage: trcli [OPTIONS] COMMAND [ARGS]...

Expand Down Expand Up @@ -1675,7 +1675,7 @@ Options:
### Reference
```shell
$ trcli add_run --help
TestRail CLI v1.14.0
TestRail CLI v1.14.1
Copyright 2025 Gurock Software GmbH - www.gurock.com
Usage: trcli add_run [OPTIONS]

Expand Down Expand Up @@ -1885,7 +1885,7 @@ providing you with a solid base of test cases, which you can further expand on T
### Reference
```shell
$ trcli parse_openapi --help
TestRail CLI v1.14.0
TestRail CLI v1.14.1
Copyright 2025 Gurock Software GmbH - www.gurock.com
Usage: trcli parse_openapi [OPTIONS]

Expand Down
63 changes: 52 additions & 11 deletions tests/test_api_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,10 +1377,10 @@ def test_upload_attachments_413_error(self, api_request_handler: ApiRequestHandl

# Prepare test data
report_results = [{"case_id": 100, "attachments": [str(test_file)]}]
case_id_to_result_id = {100: 2001}
request_id_to_result_id = {id(report_results[0]): 2001}

# Call upload_attachments
api_request_handler.upload_attachments(report_results, case_id_to_result_id)
api_request_handler.upload_attachments(report_results, request_id_to_result_id)

# Verify the request was made (case-insensitive comparison)
assert requests_mock.last_request.url.lower() == create_url("add_attachment_to_result/2001").lower()
Expand All @@ -1397,10 +1397,10 @@ def test_upload_attachments_success(self, api_request_handler: ApiRequestHandler

# Prepare test data
report_results = [{"case_id": 100, "attachments": [str(test_file)]}]
case_id_to_result_id = {100: 2001}
request_id_to_result_id = {id(report_results[0]): 2001}

# Call upload_attachments
api_request_handler.upload_attachments(report_results, case_id_to_result_id)
api_request_handler.upload_attachments(report_results, request_id_to_result_id)

# Verify the request was made (case-insensitive comparison)
assert requests_mock.last_request.url.lower() == create_url("add_attachment_to_result/2001").lower()
Expand All @@ -1410,10 +1410,10 @@ def test_upload_attachments_file_not_found(self, api_request_handler: ApiRequest
"""Test that missing attachment files are properly reported."""
# Prepare test data with non-existent file
report_results = [{"case_id": 100, "attachments": ["/path/to/nonexistent/file.jpg"]}]
case_id_to_result_id = {100: 2001}
request_id_to_result_id = {id(report_results[0]): 2001}

# Call upload_attachments - should not raise exception
api_request_handler.upload_attachments(report_results, case_id_to_result_id)
api_request_handler.upload_attachments(report_results, request_id_to_result_id)

@pytest.mark.api_handler
def test_upload_attachments_empty_run_scenario(
Expand All @@ -1423,8 +1423,8 @@ def test_upload_attachments_empty_run_scenario(

This test covers the bug fix for issue where TRCLI failed to upload attachments
when using --run-id with an empty run (created via API with include_all: false
and no case_ids). The fix uses case_id to result_id mapping instead of fetching
tests from the run.
and no case_ids). The fix uses request object identity to result_id mapping instead
of case_id mapping, which correctly handles duplicate case_ids.
"""
# Create test attachment files
attachment1 = tmp_path / "screenshot1.png"
Expand All @@ -1442,11 +1442,11 @@ def test_upload_attachments_empty_run_scenario(
{"case_id": 101, "attachments": [str(attachment2)]},
]

# Case ID to result ID mapping (this is built from add_results_for_cases response)
case_id_to_result_id = {100: 5001, 101: 5002}
# Request object ID to result ID mapping (this is built from add_results_for_cases response)
request_id_to_result_id = {id(report_results[0]): 5001, id(report_results[1]): 5002}

# Call upload_attachments
api_request_handler.upload_attachments(report_results, case_id_to_result_id)
api_request_handler.upload_attachments(report_results, request_id_to_result_id)

# Verify both attachments were uploaded correctly
history = requests_mock.request_history
Expand All @@ -1458,6 +1458,47 @@ def test_upload_attachments_empty_run_scenario(
assert any("add_attachment_to_result/5001" in url for url in urls), "Should upload to result 5001"
assert any("add_attachment_to_result/5002" in url for url in urls), "Should upload to result 5002"

@pytest.mark.api_handler
def test_upload_attachments_duplicate_case_ids_different_results(
self, api_request_handler: ApiRequestHandler, requests_mock, tmp_path
):
"""Test that attachments are uploaded to correct results when same case_id appears multiple times."""
# Create test attachment files
attachment1 = tmp_path / "report1_screenshot.png"
attachment1.write_text("screenshot from report 1")
attachment2 = tmp_path / "report2_screenshot.png"
attachment2.write_text("screenshot from report 2")

# Mock successful attachment uploads
requests_mock.post(create_url("add_attachment_to_result/1001"), status_code=200, json={"attachment_id": 9001})
requests_mock.post(create_url("add_attachment_to_result/1002"), status_code=200, json={"attachment_id": 9002})

# Prepare test data - SAME case_id (123) but different result objects with different attachments
# This simulates what happens when glob pattern processes multiple reports with the same test
result1 = {"case_id": 123, "attachments": [str(attachment1)]}
result2 = {"case_id": 123, "attachments": [str(attachment2)]}
report_results = [result1, result2]

# Request object ID to result ID mapping
# Key insight: We map by object identity, NOT by case_id
request_id_to_result_id = {
id(result1): 1001, # First occurrence of case 123
id(result2): 1002, # Second occurrence of case 123
}

# Call upload_attachments
api_request_handler.upload_attachments(report_results, request_id_to_result_id)

# Verify both attachments were uploaded correctly
history = requests_mock.request_history
upload_requests = [req for req in history if "add_attachment_to_result" in req.url]
assert len(upload_requests) == 2, "Should have uploaded 2 attachments"

# Verify attachments went to DIFFERENT result IDs (not both to 1002)
urls = [req.url.lower() for req in upload_requests]
assert any("add_attachment_to_result/1001" in url for url in urls), "Should upload attachment1 to result 1001"
assert any("add_attachment_to_result/1002" in url for url in urls), "Should upload attachment2 to result 1002"

@pytest.mark.api_handler
def test_caching_reduces_api_calls(self, api_request_handler: ApiRequestHandler, requests_mock):
"""Test that caching reduces the number of API calls for repeated requests"""
Expand Down
4 changes: 2 additions & 2 deletions tests/test_api_request_handler_case_fields_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ def test_update_case_with_only_refs_no_fields(self, handler):
assert skipped_refs == []
assert updated_fields == []

# Verify the API call included only refs
# Verify the API call included only refs and is_legacy
handler.client.send_post.assert_called_once()
call_args = handler.client.send_post.call_args
update_data = call_args[0][1]
assert update_data == {"refs": "REQ-1"}
assert update_data == {"refs": "REQ-1", "is_legacy": True}

def test_update_case_filters_internal_fields(self, handler):
"""Test that internal fields are filtered out from updates"""
Expand Down
6 changes: 4 additions & 2 deletions tests/test_api_request_handler_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,9 @@ def test_add_labels_to_cases_success(self):
mock_add_label.assert_called_once_with(1, "test-label")
assert mock_send_get.call_count == 2
# Should call update_cases/{suite_id} once for batch update
mock_send_post.assert_called_once_with("update_cases/1", payload={"case_ids": [1, 2], "labels": [5]})
mock_send_post.assert_called_once_with(
"update_cases/1", payload={"case_ids": [1, 2], "labels": [5], "is_legacy": True}
)

def test_add_labels_to_cases_single_case(self):
"""Test adding labels to a single test case using update_case endpoint"""
Expand Down Expand Up @@ -392,7 +394,7 @@ def test_add_labels_to_cases_single_case(self):
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]})
mock_send_post.assert_called_once_with("update_case/1", payload={"labels": [5], "is_legacy": True})

def test_add_labels_to_cases_existing_label(self):
"""Test adding labels when label already exists"""
Expand Down
Loading
Loading