• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

testit-tms / adapters-python / 21520479975

30 Jan 2026 03:09PM UTC coverage: 37.23% (-0.003%) from 37.233%
21520479975

push

github

web-flow
fix: fix TMS-37357 issue with externalId html (#227)

1 of 5 new or added lines in 3 files covered. (20.0%)

1328 of 3567 relevant lines covered (37.23%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.66
/testit-python-commons/src/testit_python_commons/utils/html_escape_utils.py
1
import os
2 ✔
2
import re
2 ✔
3
import logging
2 ✔
4
from typing import Any, List, Optional, Union
2 ✔
5
from datetime import datetime, date, time, timedelta
2 ✔
6
from decimal import Decimal
2 ✔
7
from uuid import UUID
2 ✔
8

9

10
class HtmlEscapeUtils:
2 ✔
11
    """
12
    HTML escape utilities for preventing XSS attacks.
13
    Escapes HTML tags in strings and objects using reflection.
14
    """
15
    
16
    NO_ESCAPE_HTML_ENV_VAR = "NO_ESCAPE_HTML"
2 ✔
17
    
18
    # Regex pattern to detect HTML tags (requires at least one non-whitespace character after <)
19
    # This ensures empty <> brackets are not considered HTML tags
20
    _HTML_TAG_PATTERN = re.compile(r'<\S.*?(?:>|/>)')
2 ✔
21
    
22
    # Regex patterns to escape only non-escaped characters
23
    # Using negative lookbehind to avoid double escaping
24
    _LESS_THAN_PATTERN = re.compile('<')
2 ✔
25
    _GREATER_THAN_PATTERN = re.compile('>')
2 ✔
26
    
27
    @staticmethod
2 ✔
28
    def escape_html_tags(text: Optional[str]) -> Optional[str]:
2 ✔
29
        """
30
        Escapes HTML tags to prevent XSS attacks.
31
        First checks if the string contains HTML tags using regex pattern.
32
        Only performs escaping if HTML tags are detected.
33
        Escapes all < as \\< and > as \\> only if they are not already escaped.
34
        Uses regex with negative lookbehind to avoid double escaping.
35
        
36
        Args:
37
            text: The text to escape
38
            
39
        Returns:
40
            Escaped text or original text if escaping is disabled
41
        """
42
        if text is None:
2 ✔
43
            return None
2 ✔
44
            
45
        # Check if escaping is disabled via environment variable
46
        no_escape_html = os.environ.get(HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR, "").lower()
2 ✔
47
        if no_escape_html == "true":
2 ✔
48
            return text
2 ✔
49
            
50
        # First check if the string contains HTML tags
51
        if not HtmlEscapeUtils._HTML_TAG_PATTERN.search(text):
2 ✔
52
            return text  # No HTML tags found, return original string
2 ✔
53
            
54
        # Use regex with negative lookbehind to escape only non-escaped characters
55
        result = HtmlEscapeUtils._LESS_THAN_PATTERN.sub('&lt;', text)
2 ✔
56
        result = HtmlEscapeUtils._GREATER_THAN_PATTERN.sub('&gt;', result)
2 ✔
57
        
58
        return result
2 ✔
59
    
60
    @staticmethod
2 ✔
61
    def escape_html_in_object(obj: Any) -> Any:
2 ✔
62
        """
63
        Escapes HTML tags in all string attributes of an object using reflection.
64
        Also processes list attributes: if list of objects - calls escape_html_in_object_list,
65
        if list of strings - escapes each string.
66
        Can be disabled by setting NO_ESCAPE_HTML environment variable to "true".
67
        
68
        Args:
69
            obj: The object to process
70
            
71
        Returns:
72
            The processed object with escaped strings
73
        """
74
        if obj is None:
2 ✔
75
            return None
×
76
            
77
        # Check if escaping is disabled via environment variable
78
        no_escape_html = os.environ.get(HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR, "").lower()
2 ✔
79
        if no_escape_html == "true":
2 ✔
80
            return obj
2 ✔
81
            
82
        try:
2 ✔
83
            HtmlEscapeUtils._process_object_attributes(obj)
2 ✔
84
        except Exception as e:
×
85
            # Silently ignore reflection errors
86
            logging.debug(f"Error processing object attributes: {e}")
×
87
            
88
        return obj
2 ✔
89
    
90
    @staticmethod
2 ✔
91
    def escape_html_in_object_list(obj_list: Optional[List[Any]]) -> Optional[List[Any]]:
2 ✔
92
        """
93
        Escapes HTML tags in all string attributes of objects in a list using reflection.
94
        Can be disabled by setting NO_ESCAPE_HTML environment variable to "true".
95
        
96
        Args:
97
            obj_list: The list of objects to process
98
            
99
        Returns:
100
            The processed list with escaped strings in all objects
101
        """
102
        if obj_list is None:
2 ✔
103
            return None
×
104
            
105
        # Check if escaping is disabled via environment variable
106
        no_escape_html = os.environ.get(HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR, "").lower()
2 ✔
107
        if no_escape_html == "true":
2 ✔
108
            return obj_list
×
109
            
110
        for obj in obj_list:
2 ✔
111
            HtmlEscapeUtils.escape_html_in_object(obj)
2 ✔
112
            
113
        return obj_list
2 ✔
114
    
115
    @staticmethod
2 ✔
116
    def _process_object_attributes(obj: Any) -> None:
2 ✔
117
        """
118
        Process all attributes of an object for HTML escaping.
119
        """
120
        # Handle dictionary-like objects (common in API models)
121
        if hasattr(obj, '__dict__'):
2 ✔
122
            for attr_name in dir(obj):
2 ✔
123
                # Skip private/protected attributes and methods
124
                if attr_name.startswith('_') or callable(getattr(obj, attr_name, None)):
2 ✔
125
                    continue
×
126
                if attr_name.startswith("external_id") or attr_name.startswith("externalId"):
2 ✔
NEW
127
                    continue
×
128
                    
129
                try:
2 ✔
130
                    value = getattr(obj, attr_name)
2 ✔
131
                    HtmlEscapeUtils._process_attribute_value(obj, attr_name, value)
2 ✔
132
                except Exception as e:
×
133
                    # Silently ignore attribute errors
134
                    logging.debug(f"Error processing attribute {attr_name}: {e}")
×
135
                    
136
        # Handle dictionary objects
137
        elif isinstance(obj, dict):
2 ✔
138
            for key, value in obj.items():
2 ✔
139
                if isinstance(value, str):
2 ✔
140
                    obj[key] = HtmlEscapeUtils.escape_html_tags(value)
2 ✔
141
                elif isinstance(value, list):
2 ✔
142
                    HtmlEscapeUtils._process_list(value)
2 ✔
143
                elif not HtmlEscapeUtils._is_simple_type(type(value)):
×
144
                    HtmlEscapeUtils.escape_html_in_object(value)
×
145
    
146
    @staticmethod
2 ✔
147
    def _process_attribute_value(obj: Any, attr_name: str, value: Any) -> None:
2 ✔
148
        """
149
        Process a single attribute value for HTML escaping.
150
        """
151
        if isinstance(value, str):
2 ✔
152
            # Escape string attributes
153
            try:
2 ✔
154
                setattr(obj, attr_name, HtmlEscapeUtils.escape_html_tags(value))
2 ✔
155
            except AttributeError:
×
156
                # Attribute might be read-only
157
                pass
×
158
        elif isinstance(value, list):
2 ✔
159
            HtmlEscapeUtils._process_list(value)
2 ✔
160
        elif value is not None and not HtmlEscapeUtils._is_simple_type(type(value)):
×
161
            # Process nested objects (but not simple types)
162
            HtmlEscapeUtils.escape_html_in_object(value)
×
163
    
164
    @staticmethod
2 ✔
165
    def _process_list(lst: List[Any]) -> None:
2 ✔
166
        """
167
        Process a list for HTML escaping.
168
        """
169
        if not lst:
2 ✔
170
            return
×
171
            
172
        first_element = lst[0]
2 ✔
173
        
174
        if isinstance(first_element, str):
2 ✔
175
            # List of strings - escape each string
176
            for i, item in enumerate(lst):
2 ✔
177
                if isinstance(item, str):
2 ✔
178
                    lst[i] = HtmlEscapeUtils.escape_html_tags(item)
2 ✔
179
        elif first_element is not None:
×
180
            # List of objects - process each object
181
            for item in lst:
×
182
                HtmlEscapeUtils.escape_html_in_object(item)
×
183
    
184
    @staticmethod
2 ✔
185
    def _is_simple_type(obj_type: type) -> bool:
2 ✔
186
        """
187
        Checks if a type is a simple type that doesn't need HTML escaping.
188
        
189
        Args:
190
            obj_type: Type to check
191
            
192
        Returns:
193
            True if it's a simple type
194
        """
195
        simple_types = {
×
196
            # Basic types
197
            bool, int, float, complex, bytes, bytearray,
198
            # String type (handled separately)
199
            str,
200
            # Date/time types
201
            datetime, date, time, timedelta,
202
            # Other common types
203
            Decimal, UUID,
204
            # None type
205
            type(None)
206
        }
207
        
208
        return (
×
209
            obj_type in simple_types or
210
            # Check for enums
211
            (hasattr(obj_type, '__bases__') and any(
212
                base.__name__ == 'Enum' for base in obj_type.__bases__
213
            ))
214
        ) 
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc