testit-python-commons 3.6.4.post530__tar.gz → 3.6.5.post530__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/PKG-INFO +1 -1
  2. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/setup.py +1 -1
  3. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/client/api_client.py +18 -1
  4. testit_python_commons-3.6.5.post530/src/testit_python_commons/utils/__init__.py +3 -0
  5. testit_python_commons-3.6.5.post530/src/testit_python_commons/utils/html_escape_utils.py +212 -0
  6. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons.egg-info/PKG-INFO +1 -1
  7. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons.egg-info/SOURCES.txt +4 -1
  8. testit_python_commons-3.6.5.post530/tests/test_html_escape_utils.py +116 -0
  9. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/README.md +0 -0
  10. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/setup.cfg +0 -0
  11. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit.py +0 -0
  12. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/__init__.py +0 -0
  13. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/app_properties.py +0 -0
  14. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/client/__init__.py +0 -0
  15. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/client/client_configuration.py +0 -0
  16. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/client/converter.py +0 -0
  17. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/decorators.py +0 -0
  18. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/dynamic_methods.py +0 -0
  19. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/models/__init__.py +0 -0
  20. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/models/adapter_mode.py +0 -0
  21. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/models/fixture.py +0 -0
  22. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/models/link.py +0 -0
  23. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/models/link_type.py +0 -0
  24. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/models/outcome_type.py +0 -0
  25. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/models/step_result.py +0 -0
  26. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/models/test_result.py +0 -0
  27. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/models/test_result_with_all_fixture_step_results_model.py +0 -0
  28. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/__init__.py +0 -0
  29. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/adapter_manager.py +0 -0
  30. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/adapter_manager_configuration.py +0 -0
  31. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/fixture_manager.py +0 -0
  32. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/fixture_storage.py +0 -0
  33. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/logger.py +0 -0
  34. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/plugin_manager.py +0 -0
  35. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/retry.py +0 -0
  36. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/step_manager.py +0 -0
  37. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/step_result_storage.py +0 -0
  38. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/services/utils.py +0 -0
  39. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons/step.py +0 -0
  40. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons.egg-info/dependency_links.txt +0 -0
  41. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons.egg-info/requires.txt +0 -0
  42. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/src/testit_python_commons.egg-info/top_level.txt +0 -0
  43. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/tests/test_app_properties.py +0 -0
  44. {testit_python_commons-3.6.4.post530 → testit_python_commons-3.6.5.post530}/tests/test_dynamic_methods.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testit-python-commons
3
- Version: 3.6.4.post530
3
+ Version: 3.6.5.post530
4
4
  Summary: Python commons for Test IT
5
5
  Home-page: https://github.com/testit-tms/adapters-python/
6
6
  Author: Integration team
@@ -1,6 +1,6 @@
1
1
  from setuptools import find_packages, setup
2
2
 
3
- VERSION = "3.6.4.post530"
3
+ VERSION = "3.6.5.post530"
4
4
 
5
5
  setup(
6
6
  name='testit-python-commons',
@@ -21,6 +21,7 @@ from testit_python_commons.client.converter import Converter
21
21
  from testit_python_commons.models.test_result import TestResult
22
22
  from testit_python_commons.services.logger import adapter_logger
23
23
  from testit_python_commons.services.retry import retry
24
+ from testit_python_commons.utils.html_escape_utils import HtmlEscapeUtils
24
25
 
25
26
 
26
27
  class ApiClientWorker:
@@ -57,6 +58,11 @@ class ApiClientWorker:
57
58
  header_name='Authorization',
58
59
  header_value='PrivateToken ' + token)
59
60
 
61
+ @staticmethod
62
+ def _escape_html_in_model(model):
63
+ """Apply HTML escaping to all models before sending to API"""
64
+ return HtmlEscapeUtils.escape_html_in_object(model)
65
+
60
66
  @adapter_logger
61
67
  def create_test_run(self):
62
68
  test_run_name = f'TestRun_{datetime.today().strftime("%Y-%m-%dT%H:%M:%S")}' if \
@@ -65,6 +71,7 @@ class ApiClientWorker:
65
71
  self.__config.get_project_id(),
66
72
  test_run_name
67
73
  )
74
+ model = self._escape_html_in_model(model)
68
75
 
69
76
  response = self.__test_run_api.create_empty(create_empty_test_run_api_model=model)
70
77
 
@@ -288,6 +295,7 @@ class ApiClientWorker:
288
295
  logging.debug(f'Autotest "{test_result.get_autotest_name()}" was not found')
289
296
 
290
297
  model = self.__prepare_to_create_autotest(test_result)
298
+ model = self._escape_html_in_model(model)
291
299
 
292
300
  autotest_response = self.__autotest_api.create_auto_test(auto_test_post_model=model)
293
301
 
@@ -299,6 +307,7 @@ class ApiClientWorker:
299
307
  def __create_tests(self, autotests_for_create: typing.List[AutoTestPostModel]):
300
308
  logging.debug(f'Creating autotests: "{autotests_for_create}')
301
309
 
310
+ autotests_for_create = self._escape_html_in_model(autotests_for_create)
302
311
  self.__autotest_api.create_multiple(auto_test_post_model=autotests_for_create)
303
312
 
304
313
  logging.debug(f'Autotests were created')
@@ -308,6 +317,7 @@ class ApiClientWorker:
308
317
  logging.debug(f'Autotest "{test_result.get_autotest_name()}" was found')
309
318
 
310
319
  model = self.__prepare_to_update_autotest(test_result, autotest)
320
+ model = self._escape_html_in_model(model)
311
321
 
312
322
  self.__autotest_api.update_auto_test(auto_test_put_model=model)
313
323
 
@@ -317,6 +327,7 @@ class ApiClientWorker:
317
327
  def __update_tests(self, autotests_for_update: typing.List[AutoTestPutModel]):
318
328
  logging.debug(f'Updating autotests: {autotests_for_update}')
319
329
 
330
+ autotests_for_update = self._escape_html_in_model(autotests_for_update)
320
331
  self.__autotest_api.update_multiple(auto_test_put_model=autotests_for_update)
321
332
 
322
333
  logging.debug(f'Autotests were updated')
@@ -344,6 +355,7 @@ class ApiClientWorker:
344
355
  model = Converter.test_result_to_testrun_result_post_model(
345
356
  test_result,
346
357
  self.__config.get_configuration_id())
358
+ model = self._escape_html_in_model(model)
347
359
 
348
360
  response = self.__test_run_api.set_auto_test_results_for_test_run(
349
361
  id=self.__config.get_test_run_id(),
@@ -358,6 +370,7 @@ class ApiClientWorker:
358
370
  def __load_test_results(self, test_results: typing.List[AutoTestResultsForTestRunModel]):
359
371
  logging.debug(f'Loading test results: {test_results}')
360
372
 
373
+ test_results = self._escape_html_in_model(test_results)
361
374
  self.__test_run_api.set_auto_test_results_for_test_run(
362
375
  id=self.__config.get_test_run_id(),
363
376
  auto_test_results_for_test_run_model=test_results)
@@ -379,6 +392,8 @@ class ApiClientWorker:
379
392
  test_result.get_setup_results())
380
393
  model.teardown_results = Converter.step_results_to_auto_test_step_result_update_request(
381
394
  test_result.get_teardown_results())
395
+
396
+ model = self._escape_html_in_model(model)
382
397
 
383
398
  try:
384
399
  self.__test_results_api.api_v2_test_results_id_put(
@@ -397,7 +412,9 @@ class ApiClientWorker:
397
412
  attachment_response = self.__attachments_api.api_v2_attachments_post(
398
413
  file=path)
399
414
 
400
- attachments.append(AttachmentPutModel(id=attachment_response.id))
415
+ attachment_model = AttachmentPutModel(id=attachment_response.id)
416
+ attachment_model = self._escape_html_in_model(attachment_model)
417
+ attachments.append(attachment_model)
401
418
 
402
419
  logging.debug(f'Attachment "{path}" was uploaded')
403
420
  except Exception as exc:
@@ -0,0 +1,3 @@
1
+ from .html_escape_utils import HtmlEscapeUtils
2
+
3
+ __all__ = ['HtmlEscapeUtils']
@@ -0,0 +1,212 @@
1
+ import os
2
+ import re
3
+ import logging
4
+ from typing import Any, List, Optional, Union
5
+ from datetime import datetime, date, time, timedelta
6
+ from decimal import Decimal
7
+ from uuid import UUID
8
+
9
+
10
+ class HtmlEscapeUtils:
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"
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.*?(?:>|/>)')
21
+
22
+ # Regex patterns to escape only non-escaped characters
23
+ # Using negative lookbehind to avoid double escaping
24
+ _LESS_THAN_PATTERN = re.compile(r'(?<!\\)<')
25
+ _GREATER_THAN_PATTERN = re.compile(r'(?<!\\)>')
26
+
27
+ @staticmethod
28
+ def escape_html_tags(text: Optional[str]) -> Optional[str]:
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:
43
+ return None
44
+
45
+ # Check if escaping is disabled via environment variable
46
+ no_escape_html = os.environ.get(HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR, "").lower()
47
+ if no_escape_html == "true":
48
+ return text
49
+
50
+ # First check if the string contains HTML tags
51
+ if not HtmlEscapeUtils._HTML_TAG_PATTERN.search(text):
52
+ return text # No HTML tags found, return original string
53
+
54
+ # Use regex with negative lookbehind to escape only non-escaped characters
55
+ result = HtmlEscapeUtils._LESS_THAN_PATTERN.sub(r'\\<', text)
56
+ result = HtmlEscapeUtils._GREATER_THAN_PATTERN.sub(r'\\>', result)
57
+
58
+ return result
59
+
60
+ @staticmethod
61
+ def escape_html_in_object(obj: Any) -> Any:
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:
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()
79
+ if no_escape_html == "true":
80
+ return obj
81
+
82
+ try:
83
+ HtmlEscapeUtils._process_object_attributes(obj)
84
+ except Exception as e:
85
+ # Silently ignore reflection errors
86
+ logging.debug(f"Error processing object attributes: {e}")
87
+
88
+ return obj
89
+
90
+ @staticmethod
91
+ def escape_html_in_object_list(obj_list: Optional[List[Any]]) -> Optional[List[Any]]:
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:
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()
107
+ if no_escape_html == "true":
108
+ return obj_list
109
+
110
+ for obj in obj_list:
111
+ HtmlEscapeUtils.escape_html_in_object(obj)
112
+
113
+ return obj_list
114
+
115
+ @staticmethod
116
+ def _process_object_attributes(obj: Any) -> None:
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__'):
122
+ for attr_name in dir(obj):
123
+ # Skip private/protected attributes and methods
124
+ if attr_name.startswith('_') or callable(getattr(obj, attr_name, None)):
125
+ continue
126
+
127
+ try:
128
+ value = getattr(obj, attr_name)
129
+ HtmlEscapeUtils._process_attribute_value(obj, attr_name, value)
130
+ except Exception as e:
131
+ # Silently ignore attribute errors
132
+ logging.debug(f"Error processing attribute {attr_name}: {e}")
133
+
134
+ # Handle dictionary objects
135
+ elif isinstance(obj, dict):
136
+ for key, value in obj.items():
137
+ if isinstance(value, str):
138
+ obj[key] = HtmlEscapeUtils.escape_html_tags(value)
139
+ elif isinstance(value, list):
140
+ HtmlEscapeUtils._process_list(value)
141
+ elif not HtmlEscapeUtils._is_simple_type(type(value)):
142
+ HtmlEscapeUtils.escape_html_in_object(value)
143
+
144
+ @staticmethod
145
+ def _process_attribute_value(obj: Any, attr_name: str, value: Any) -> None:
146
+ """
147
+ Process a single attribute value for HTML escaping.
148
+ """
149
+ if isinstance(value, str):
150
+ # Escape string attributes
151
+ try:
152
+ setattr(obj, attr_name, HtmlEscapeUtils.escape_html_tags(value))
153
+ except AttributeError:
154
+ # Attribute might be read-only
155
+ pass
156
+ elif isinstance(value, list):
157
+ HtmlEscapeUtils._process_list(value)
158
+ elif value is not None and not HtmlEscapeUtils._is_simple_type(type(value)):
159
+ # Process nested objects (but not simple types)
160
+ HtmlEscapeUtils.escape_html_in_object(value)
161
+
162
+ @staticmethod
163
+ def _process_list(lst: List[Any]) -> None:
164
+ """
165
+ Process a list for HTML escaping.
166
+ """
167
+ if not lst:
168
+ return
169
+
170
+ first_element = lst[0]
171
+
172
+ if isinstance(first_element, str):
173
+ # List of strings - escape each string
174
+ for i, item in enumerate(lst):
175
+ if isinstance(item, str):
176
+ lst[i] = HtmlEscapeUtils.escape_html_tags(item)
177
+ elif first_element is not None:
178
+ # List of objects - process each object
179
+ for item in lst:
180
+ HtmlEscapeUtils.escape_html_in_object(item)
181
+
182
+ @staticmethod
183
+ def _is_simple_type(obj_type: type) -> bool:
184
+ """
185
+ Checks if a type is a simple type that doesn't need HTML escaping.
186
+
187
+ Args:
188
+ obj_type: Type to check
189
+
190
+ Returns:
191
+ True if it's a simple type
192
+ """
193
+ simple_types = {
194
+ # Basic types
195
+ bool, int, float, complex, bytes, bytearray,
196
+ # String type (handled separately)
197
+ str,
198
+ # Date/time types
199
+ datetime, date, time, timedelta,
200
+ # Other common types
201
+ Decimal, UUID,
202
+ # None type
203
+ type(None)
204
+ }
205
+
206
+ return (
207
+ obj_type in simple_types or
208
+ # Check for enums
209
+ (hasattr(obj_type, '__bases__') and any(
210
+ base.__name__ == 'Enum' for base in obj_type.__bases__
211
+ ))
212
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testit-python-commons
3
- Version: 3.6.4.post530
3
+ Version: 3.6.5.post530
4
4
  Summary: Python commons for Test IT
5
5
  Home-page: https://github.com/testit-tms/adapters-python/
6
6
  Author: Integration team
@@ -35,5 +35,8 @@ src/testit_python_commons/services/retry.py
35
35
  src/testit_python_commons/services/step_manager.py
36
36
  src/testit_python_commons/services/step_result_storage.py
37
37
  src/testit_python_commons/services/utils.py
38
+ src/testit_python_commons/utils/__init__.py
39
+ src/testit_python_commons/utils/html_escape_utils.py
38
40
  tests/test_app_properties.py
39
- tests/test_dynamic_methods.py
41
+ tests/test_dynamic_methods.py
42
+ tests/test_html_escape_utils.py
@@ -0,0 +1,116 @@
1
+ import os
2
+ import unittest
3
+ from testit_python_commons.utils.html_escape_utils import HtmlEscapeUtils
4
+
5
+
6
+ class SampleData:
7
+ def __init__(self, name="Test Name", description="<script>alert('xss')</script>"):
8
+ self.name = name
9
+ self.description = description
10
+ self.tags = ["<tag>", "normal_tag"]
11
+
12
+
13
+ class TestHtmlEscapeUtils(unittest.TestCase):
14
+
15
+ def test_escape_html_tags_basic(self):
16
+ """Test basic HTML tag escaping"""
17
+ text = "Hello <script>alert('test')</script> world"
18
+ result = HtmlEscapeUtils.escape_html_tags(text)
19
+ expected = "Hello \\<script\\>alert('test')\\</script\\> world"
20
+ self.assertEqual(result, expected)
21
+
22
+ def test_escape_html_tags_no_html_content(self):
23
+ """Test that strings without HTML tags are returned unchanged"""
24
+ text = "Just plain text without any tags"
25
+ result = HtmlEscapeUtils.escape_html_tags(text)
26
+ self.assertEqual(result, text) # Should be identical object
27
+
28
+ text_with_empty_brackets = "Text with & symbols and other <> but not HTML tags"
29
+ result = HtmlEscapeUtils.escape_html_tags(text_with_empty_brackets)
30
+ # This should remain unchanged because <> is not a valid HTML tag
31
+ self.assertEqual(result, text_with_empty_brackets)
32
+
33
+
34
+ def test_escape_html_tags_none(self):
35
+ """Test handling of None input"""
36
+ result = HtmlEscapeUtils.escape_html_tags(None)
37
+ self.assertIsNone(result)
38
+
39
+ def test_escape_html_tags_no_double_escaping(self):
40
+ """Test that already escaped characters are not double-escaped"""
41
+ text = "Already \\<escaped\\> and <not_escaped>"
42
+ result = HtmlEscapeUtils.escape_html_tags(text)
43
+ expected = "Already \\<escaped\\> and \\<not_escaped\\>"
44
+ self.assertEqual(result, expected)
45
+
46
+ def test_escape_html_tags_various_tags(self):
47
+ """Test escaping of various HTML tag types"""
48
+ test_cases = [
49
+ ("<div>content</div>", "\\<div\\>content\\</div\\>"),
50
+ ("<img src='test.jpg'/>", "\\<img src='test.jpg'/\\>"),
51
+ ("<br>", "\\<br\\>"),
52
+ ("<span class='test'>text</span>", "\\<span class='test'\\>text\\</span\\>"),
53
+ ("No tags here", "No tags here"), # Should remain unchanged
54
+ ("<>", "<>"), # Empty angle brackets - should remain unchanged
55
+ ("< >", "< >"), # Spaced angle brackets - should remain unchanged
56
+ ]
57
+
58
+ for input_text, expected in test_cases:
59
+ with self.subTest(input_text=input_text):
60
+ result = HtmlEscapeUtils.escape_html_tags(input_text)
61
+ self.assertEqual(result, expected)
62
+
63
+ def test_escape_html_in_object(self):
64
+ """Test HTML escaping in object attributes"""
65
+ test_obj = SampleData()
66
+ result = HtmlEscapeUtils.escape_html_in_object(test_obj)
67
+
68
+ self.assertEqual(result.name, "Test Name") # No escaping needed
69
+ self.assertEqual(result.description, "\\<script\\>alert('xss')\\</script\\>") # Escaped
70
+ self.assertEqual(result.tags[0], "\\<tag\\>") # Escaped
71
+ self.assertEqual(result.tags[1], "normal_tag") # No escaping needed
72
+
73
+ def test_escape_html_in_object_list(self):
74
+ """Test HTML escaping in list of objects"""
75
+ test_list = [
76
+ SampleData("First", "<div>content</div>"),
77
+ SampleData("Second", "<span>more</span>")
78
+ ]
79
+ result = HtmlEscapeUtils.escape_html_in_object_list(test_list)
80
+
81
+ self.assertEqual(result[0].description, "\\<div\\>content\\</div\\>")
82
+ self.assertEqual(result[1].description, "\\<span\\>more\\</span\\>")
83
+
84
+ def test_escape_disabled_by_env_var(self):
85
+ """Test that escaping can be disabled via environment variable"""
86
+ os.environ[HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR] = "true"
87
+
88
+ try:
89
+ text = "Hello <script>alert('test')</script> world"
90
+ result = HtmlEscapeUtils.escape_html_tags(text)
91
+ self.assertEqual(result, text) # Should be unchanged
92
+
93
+ test_obj = SampleData()
94
+ result = HtmlEscapeUtils.escape_html_in_object(test_obj)
95
+ self.assertEqual(result.description, "<script>alert('xss')</script>") # Should be unchanged
96
+ finally:
97
+ # Clean up environment variable
98
+ del os.environ[HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR]
99
+
100
+ def test_escape_html_in_dictionary(self):
101
+ """Test HTML escaping in dictionary objects"""
102
+ test_dict = {
103
+ "name": "Test",
104
+ "description": "<script>alert('xss')</script>",
105
+ "tags": ["<tag>", "normal_tag"]
106
+ }
107
+ result = HtmlEscapeUtils.escape_html_in_object(test_dict)
108
+
109
+ self.assertEqual(result["name"], "Test")
110
+ self.assertEqual(result["description"], "\\<script\\>alert('xss')\\</script\\>")
111
+ self.assertEqual(result["tags"][0], "\\<tag\\>")
112
+ self.assertEqual(result["tags"][1], "normal_tag")
113
+
114
+
115
+ if __name__ == '__main__':
116
+ unittest.main()