Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

trcli

Package Overview
Dependencies
Maintainers
1
Versions
45
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

trcli - npm Package Compare versions

Comparing version
1.12.2
to
1.12.3
+1
-1
PKG-INFO
Metadata-Version: 2.4
Name: trcli
Version: 1.12.2
Version: 1.12.3
License-File: LICENSE.md

@@ -5,0 +5,0 @@ Requires-Dist: click<8.2.2,>=8.1.0

@@ -36,3 +36,3 @@ ![Tests](https://github.com/gurock/trcli/actions/workflows/python-app.yml/badge.svg)

```
TestRail CLI v1.12.2
TestRail CLI v1.12.3
Copyright 2025 Gurock Software GmbH - www.gurock.com

@@ -51,3 +51,3 @@ Supported and loaded modules:

$ trcli --help
TestRail CLI v1.12.1
TestRail CLI v1.12.3
Copyright 2025 Gurock Software GmbH - www.gurock.com

@@ -1099,3 +1099,3 @@ Usage: trcli [OPTIONS] COMMAND [ARGS]...

$ trcli add_run --help
TestRail CLI v1.12.1
TestRail CLI v1.12.3
Copyright 2025 Gurock Software GmbH - www.gurock.com

@@ -1224,3 +1224,3 @@ Usage: trcli add_run [OPTIONS]

$ trcli parse_openapi --help
TestRail CLI v1.12.1
TestRail CLI v1.12.3
Copyright 2025 Gurock Software GmbH - www.gurock.com

@@ -1227,0 +1227,0 @@ Usage: trcli parse_openapi [OPTIONS]

@@ -48,5 +48,3 @@ import pytest

Check that response was packed into APIClientResult, retry mechanism was not triggered."""
requests_mock.get(
create_url("get_projects"), status_code=200, json=FAKE_PROJECT_DATA
)
requests_mock.get(create_url("get_projects"), status_code=200, json=FAKE_PROJECT_DATA)
api_client = api_resources

@@ -62,5 +60,3 @@ response = api_client.send_get("get_projects")

Check that response was packed into APIClientResult, retry mechanism was not triggered."""
requests_mock.post(
create_url("add_project"), status_code=201, json=FAKE_PROJECT_DATA
)
requests_mock.post(create_url("add_project"), status_code=201, json=FAKE_PROJECT_DATA)
api_client = api_resources

@@ -87,5 +83,3 @@ response = api_client.send_post("add_project", FAKE_PROJECT_DATA)

check_calls_count(requests_mock)
check_response(
400, INVALID_TEST_CASE_ERROR, INVALID_TEST_CASE_ERROR["error"], response
)
check_response(400, INVALID_TEST_CASE_ERROR, INVALID_TEST_CASE_ERROR["error"], response)

@@ -96,5 +90,3 @@ @pytest.mark.api_client

Check that response was packed into APIClientResult, retry mechanism was not triggered."""
requests_mock.post(
create_url("add_project"), status_code=403, json=NO_PERMISSION_PROJECT_ERROR
)
requests_mock.post(create_url("add_project"), status_code=403, json=NO_PERMISSION_PROJECT_ERROR)
api_client = api_resources

@@ -113,5 +105,3 @@ response = api_client.send_post("add_project", {"fake_project_data": "data"})

@pytest.mark.parametrize("retries, retry_after", [(4, "30"), (10, "60")])
def test_retry_mechanism_too_many_requests(
self, retries, retry_after, api_resources_maker, requests_mock, mocker
):
def test_retry_mechanism_too_many_requests(self, retries, retry_after, api_resources_maker, requests_mock, mocker):
"""The purpose of this test is to check that retry mechanism will work as expected when

@@ -169,2 +159,5 @@ 429 - too many requests will be received as an answer on get request."""

f"url: https://FakeTestRail.io/index.php?/api/v2/get_projects\n"
f"headers:\n"
f" User-Agent: TRCLI\n"
f" Content-Type: application/json\n"
),

@@ -191,2 +184,5 @@ ]

f"url: https://FakeTestRail.io/index.php?/api/v2/get_projects\n"
f"headers:\n"
f" User-Agent: TRCLI\n"
f" Content-Type: application/json\n"
)

@@ -200,5 +196,3 @@ ]

"",
FAULT_MAPPING["unexpected_error_during_request_send"].format(
request=request
),
FAULT_MAPPING["unexpected_error_during_request_send"].format(request=request),
response,

@@ -247,3 +241,4 @@ )

check_calls_count(requests_mock)
check_response(200, content, content, response)
expected_error_message = FAULT_MAPPING["invalid_json_response"].format(status_code=200, response_preview="Test")
check_response(200, str(content), expected_error_message, response)

@@ -260,2 +255,5 @@ @pytest.mark.api_client

f"url: https://FakeTestRail.io/index.php?/api/v2/get_projects\n"
f"headers:\n"
f" User-Agent: TRCLI\n"
f" Content-Type: application/json\n"
f"response status code: 200\n"

@@ -283,5 +281,3 @@ f"response body: ['test', 'list']\n"

)
def test_timeout_is_parsed_and_validated(
self, timeout_value, expected_message, api_resources_maker, mocker
):
def test_timeout_is_parsed_and_validated(self, timeout_value, expected_message, api_resources_maker, mocker):
environment = mocker.patch("trcli.cli.Environment")

@@ -297,3 +293,3 @@ api_client = api_resources_maker(environment=environment, timeout=timeout_value)

@pytest.mark.api_client
@patch('requests.post')
@patch("requests.post")
def test_send_post_with_json_default(self, mock_post, api_resources_maker):

@@ -317,18 +313,18 @@ """Test that send_post uses JSON by default"""

assert result.response_text == {"id": 1, "title": "Test"}
# Verify JSON was used
mock_post.assert_called_once()
call_args = mock_post.call_args
# Should use json parameter, not data
assert 'json' in call_args[1]
assert 'data' not in call_args[1]
assert call_args[1]['json'] == {":title": "Test Label"}
assert "json" in call_args[1]
assert "data" not in call_args[1]
assert call_args[1]["json"] == {":title": "Test Label"}
# Should have JSON content type header
headers = call_args[1]['headers']
assert headers.get('Content-Type') == 'application/json'
headers = call_args[1]["headers"]
assert headers.get("Content-Type") == "application/json"
@pytest.mark.api_client
@patch('requests.post')
@patch("requests.post")
def test_send_post_with_form_data_true(self, mock_post, api_resources_maker):

@@ -356,17 +352,17 @@ """Test that send_post uses form-data when as_form_data=True"""

call_args = mock_post.call_args
# Should use data parameter, not json
assert 'data' in call_args[1]
assert 'json' not in call_args[1]
assert call_args[1]['data'] == {":title": "Test Label"}
assert "data" in call_args[1]
assert "json" not in call_args[1]
assert call_args[1]["data"] == {":title": "Test Label"}
# Should NOT have files parameter (uses application/x-www-form-urlencoded)
assert 'files' not in call_args[1] or call_args[1]['files'] is None
assert "files" not in call_args[1] or call_args[1]["files"] is None
# Should NOT have JSON content type header when using form-data
headers = call_args[1]['headers']
assert headers.get('Content-Type') != 'application/json'
headers = call_args[1]["headers"]
assert headers.get("Content-Type") != "application/json"
@pytest.mark.api_client
@patch('requests.post')
@patch("requests.post")
def test_send_post_with_form_data_false(self, mock_post, api_resources_maker):

@@ -394,14 +390,14 @@ """Test that send_post uses JSON when as_form_data=False explicitly"""

call_args = mock_post.call_args
# Should use json parameter, not data
assert 'json' in call_args[1]
assert 'data' not in call_args[1]
assert call_args[1]['json'] == {":title": "Test Label"}
assert "json" in call_args[1]
assert "data" not in call_args[1]
assert call_args[1]["json"] == {":title": "Test Label"}
# Should have JSON content type header
headers = call_args[1]['headers']
assert headers.get('Content-Type') == 'application/json'
headers = call_args[1]["headers"]
assert headers.get("Content-Type") == "application/json"
@pytest.mark.api_client
@patch('requests.post')
@patch("requests.post")
def test_send_post_with_files_and_form_data(self, mock_post, api_resources_maker):

@@ -421,8 +417,3 @@ """Test that send_post handles files parameter with form-data"""

files = {"file1": "/path/to/file"}
result = api_client.send_post(
"test_endpoint",
{":title": "Test Label"},
files=files,
as_form_data=True
)
result = api_client.send_post("test_endpoint", {":title": "Test Label"}, files=files, as_form_data=True)

@@ -436,12 +427,12 @@ # Verify the result

call_args = mock_post.call_args
# Should use data parameter, not json
assert 'data' in call_args[1]
assert 'json' not in call_args[1]
assert call_args[1]['data'] == {":title": "Test Label"}
assert "data" in call_args[1]
assert "json" not in call_args[1]
assert call_args[1]["data"] == {":title": "Test Label"}
# Files should be passed through as provided (not replaced with empty dict)
assert call_args[1]['files'] == files
assert call_args[1]["files"] == files
# Should NOT have JSON content type header when using files
headers = call_args[1]['headers']
assert headers.get('Content-Type') != 'application/json'
headers = call_args[1]["headers"]
assert headers.get("Content-Type") != "application/json"
Metadata-Version: 2.4
Name: trcli
Version: 1.12.2
Version: 1.12.3
License-File: LICENSE.md

@@ -5,0 +5,0 @@ Requires-Dist: click<8.2.2,>=8.1.0

@@ -1,1 +0,1 @@

__version__ = "1.12.2"
__version__ = "1.12.3"
import json
from pathlib import Path
import platform
import os
import base64

@@ -52,3 +55,4 @@ import requests

proxy_user: str = None,
noproxy: str = None,
noproxy: str = None,
uploader_metadata: str = None,
):

@@ -66,3 +70,4 @@ self.username = ""

self.proxy_user = proxy_user
self.noproxy = noproxy.split(',') if noproxy else []
self.noproxy = noproxy.split(',') if noproxy else []
self.uploader_metadata = uploader_metadata

@@ -104,2 +109,3 @@ if not host_name.endswith("/"):

headers.update(self.__get_proxy_headers())
headers.update(self.__get_uploader_metadata_headers())
if files is None and not as_form_data:

@@ -113,3 +119,3 @@ headers["Content-Type"] = "application/json"

verbose_log_message = APIClient.format_request_for_vlog(
method=method, url=url, payload=payload
method=method, url=url, payload=payload, headers=headers
)

@@ -184,4 +190,8 @@ if method == "POST":

except (JSONDecodeError, ValueError):
response_preview = response.content[:200].decode('utf-8', errors='ignore')
response_text = str(response.content)
error_message = response.content
error_message = FAULT_MAPPING["invalid_json_response"].format(
status_code=status_code,
response_preview=response_preview
)
except AttributeError:

@@ -217,2 +227,11 @@ error_message = ""

def __get_uploader_metadata_headers(self) -> Dict[str, str]:
"""
Returns headers for uploader metadata.
"""
headers = {}
if self.uploader_metadata:
headers["X-Uploader-Metadata"] = self.uploader_metadata
return headers
def _get_proxies_for_request(self, url: str) -> Dict[str, str]:

@@ -286,8 +305,34 @@ """

@staticmethod
def format_request_for_vlog(method: str, url: str, payload: dict):
return (
def build_uploader_metadata(version: str) -> str:
"""
Build uploader metadata as base64-encoded JSON.
:param version: Application version
:returns: Base64-encoded metadata string
"""
data = {
"app_name": "trcli",
"app_version": version,
"os": platform.system().lower(),
"arch": platform.machine(),
"run_mode": "ci" if os.getenv("CI") else "other",
"container": os.path.exists("/.dockerenv"),
}
return base64.b64encode(json.dumps(data).encode()).decode()
@staticmethod
def format_request_for_vlog(method: str, url: str, payload: dict, headers: dict = None):
log_message = (
f"\n**** API Call\n"
f"method: {method}\n"
f"url: {url}\n" + (f"payload: {payload}\n" if payload else "")
f"url: {url}\n"
)
if headers:
log_message += "headers:\n"
for key, value in headers.items():
log_message += f" {key}: {value}\n"
if payload:
log_message += f"payload: {payload}\n"
return log_message

@@ -294,0 +339,0 @@ @staticmethod

@@ -9,2 +9,3 @@ from beartype.typing import Callable, Optional, Tuple

from trcli.data_classes.dataclass_testrail import TestRailSuite
import trcli

@@ -40,23 +41,21 @@

proxy_user = self.environment.proxy_user
# Generate uploader metadata
uploader_metadata = APIClient.build_uploader_metadata(version=trcli.__version__)
# Build client configuration
client_kwargs = {
"verbose_logging_function": verbose_logging_function,
"logging_function": logging_function,
"verify": not self.environment.insecure,
"proxy": proxy,
"proxy_user": proxy_user,
"noproxy": noproxy,
"uploader_metadata": uploader_metadata
}
if self.environment.timeout:
api_client = APIClient(
self.environment.host,
verbose_logging_function=verbose_logging_function,
logging_function=logging_function,
timeout=self.environment.timeout,
verify=not self.environment.insecure,
proxy=proxy,
proxy_user=proxy_user,
noproxy=noproxy
)
else:
api_client = APIClient(
self.environment.host,
logging_function=logging_function,
verbose_logging_function=verbose_logging_function,
verify=not self.environment.insecure,
proxy=proxy,
proxy_user=proxy_user,
noproxy=noproxy
)
client_kwargs["timeout"] = self.environment.timeout
api_client = APIClient(self.environment.host, **client_kwargs)
api_client.username = self.environment.username

@@ -63,0 +62,0 @@ api_client.password = self.environment.password

@@ -68,3 +68,7 @@ import trcli

no_proxy_match_error= "The host {host} does not match any NO_PROXY rules. Ensure the correct domains or IP addresses are specified for bypassing the proxy.",
no_suites_found= "The project {project_id} does not have any suites."
no_suites_found= "The project {project_id} does not have any suites.",
invalid_json_response= "Received invalid response from TestRail server (HTTP {status_code}). "
"Please verify your TestRail host URL (-h) is correct and points to a valid TestRail instance. "
"Response preview: {response_preview}",
invalid_api_response= "Invalid response from TestRail API: {error_details}"
)

@@ -71,0 +75,0 @@

Sorry, the diff of this file is too big to display