Skip to content
Open
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
49 changes: 49 additions & 0 deletions tests/test_response_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
"<p>IntegrationSuiteNew.schema-controller&nbsp;&nbsp;:: Should fail (400)</p>\n",
),
# Simple wrap with no whitespace entities.
("plain value", "<p>plain value</p>"),
# No trailing newline after the closing tag.
("plain value", "<p>plain value</p>"),
# Single leading/trailing nbsp only (no <p> wrapping).
("value", "&nbsp;value&nbsp;"),
# 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", "<p>completely different value</p>"),
# 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):
"""
Expand Down
42 changes: 41 additions & 1 deletion trcli/api/api_response_verify.py
Original file line number Diff line number Diff line change
@@ -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 <p>...</p> block, convert runs of whitespace into
# &nbsp; 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*<p>(.*)</p>\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 <p>...</p> pair and decodes HTML entities
(e.g. &nbsp;). 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)
# &nbsp; 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.
Expand Down Expand Up @@ -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 "<p>...&nbsp;...</p>\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:
Expand Down
Loading