From ff24f4ea7f660d741b5c037077afdc8a8e11cf70 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Fri, 9 Oct 2026 20:09:34 +0800 Subject: [PATCH] TR-22322: Fixed --verify data verification false failures from TestRail's legacy HTML field wrapping --- tests/test_response_verify.py | 49 ++++++++++++++++++++++++++++++++ trcli/api/api_response_verify.py | 42 ++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/tests/test_response_verify.py b/tests/test_response_verify.py index 8c93158..7e1a817 100644 --- a/tests/test_response_verify.py +++ b/tests/test_response_verify.py @@ -80,6 +80,55 @@ def test_verify_estimate( input_data_estimate, response_data_estimate ), "Added data and returned data should match" + @pytest.mark.verifier + @pytest.mark.parametrize( + "added_value, returned_value", + [ + ( + "IntegrationSuiteNew.schema-controller :: Should fail (400)", + "

IntegrationSuiteNew.schema-controller  :: Should fail (400)

\n", + ), + # Simple wrap with no whitespace entities. + ("plain value", "

plain value

"), + # No trailing newline after the closing tag. + ("plain value", "

plain value

"), + # Single leading/trailing nbsp only (no

wrapping). + ("value", " value "), + # No transformation at all - still must match. + ("identical", "identical"), + ], + ) + def test_verify_simple_comparison_tolerates_legacy_html_wrapping( + self, api_response_verify: ApiResponseVerify, added_value, returned_value + ): + added_data = {"custom_automation_id": added_value} + returned_data = {"custom_automation_id": returned_value} + + assert api_response_verify.verify_returned_data( + added_data, returned_data + ), "TestRail's is_legacy HTML wrapping should not cause a false verification failure" + + @pytest.mark.verifier + @pytest.mark.parametrize( + "added_value, returned_value", + [ + # Genuinely different content must still fail, even if HTML-wrapped. + ("expected value", "

completely different value

"), + # Non-string types must never go through the HTML-tolerant fallback. + (True, False), + (1, 2), + ], + ) + def test_verify_simple_comparison_still_fails_on_real_mismatch( + self, api_response_verify: ApiResponseVerify, added_value, returned_value + ): + added_data = {"custom_automation_id": added_value} + returned_data = {"custom_automation_id": returned_value} + + assert not api_response_verify.verify_returned_data( + added_data, returned_data + ), "Genuine content mismatches must still fail verification" + @pytest.mark.verifier def test_verify_returned_data_missing_key_fails_gracefully(self, api_response_verify: ApiResponseVerify): """ diff --git a/trcli/api/api_response_verify.py b/trcli/api/api_response_verify.py index e0637d7..29c8256 100644 --- a/trcli/api/api_response_verify.py +++ b/trcli/api/api_response_verify.py @@ -1,9 +1,40 @@ +import html +import re + from serde.json import to_dict from beartype.typing import List, Union, Any, Callable from humanfriendly import parse_timespan _MISSING = object() +# TestRail (v9.8.1+) can wrap/transform plain-text custom field values when a +# write request is sent with `is_legacy: True` (TRCLI sends this to request +# Markdown->HTML conversion for rich-text fields). This can wrap even plain +# Text fields in a single

...

block, convert runs of whitespace into +#   entities, and append a trailing newline - none of which represent an +# actual content change. Strip that formatting before comparing so --verify +# doesn't false-positive on it. +_WRAPPING_P_TAG_RE = re.compile(r"^\s*

(.*)

\s*$", re.DOTALL) + + +def _strip_legacy_html_wrapping(value: str) -> str: + """Undo TestRail's is_legacy-driven HTML wrapping of plain-text field values. + + Only unwraps a single outer

...

pair and decodes HTML entities + (e.g.  ). Does not strip arbitrary/nested HTML, so genuinely + HTML-formatted content still compares meaningfully instead of silently + matching unrelated values. + """ + match = _WRAPPING_P_TAG_RE.match(value) + if match: + value = match.group(1) + value = html.unescape(value) + #   decodes to U+00A0 (non-breaking space), not a regular space - normalize + # it back to a regular space since it represents the same whitespace semantically + # (TestRail substitutes it for whitespace runs during Markdown->HTML conversion). + value = value.replace("\xa0", " ") + return value.strip() + class ApiResponseVerify: """Class for verifying if new resources added to Test Rail are created correctly. @@ -60,7 +91,16 @@ def field_compare(self, added_data_key: str) -> Callable: @staticmethod def __simple_comparison(returned_value: Any, added_value: Any) -> bool: - return returned_value == added_value + if returned_value == added_value: + return True + # Fall back to a legacy-HTML-tolerant comparison only for string values - + # covers e.g. custom_automation_id coming back wrapped as "

... ...

\n" + # when TRCLI's `is_legacy` flag triggered TestRail's Markdown->HTML conversion + # on a plain-text field. Non-string types (int, bool, None, etc.) always use + # strict equality above - this fallback never changes their result. + if isinstance(returned_value, str) and isinstance(added_value, str): + return _strip_legacy_html_wrapping(returned_value) == _strip_legacy_html_wrapping(added_value) + return False @staticmethod def __compare_estimate(returned_value: str, added_value: str) -> bool: