Skip to content

Commit e7a3ee0

Browse files
author
Saurabh Badenkal
committed
Add robustness edge case tests from deep audit
7 new tests in TestRobustnessEdgeCases covering: - Malformed JSON body in batch response (silently handled) - Truncated JSON body (silently handled) - Exception in changeset context manager (changeset still in items) - Empty string table name (accepted, validated downstream) - Single-quote escaping in OData filter (_escape_odata_quotes) - Non-dict JSON body (list) in response (data stays None) - Boundary strings with special characters (+, /) Verified by systematic audit: - All user values in OData filters use _escape_odata_quotes - No bare except in batch code (only except Exception with fallback) - No mutable default arguments - json.dumps handles serialization safely - requests library auto-encodes URLs for non-batch path
1 parent a1990db commit e7a3ee0

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

tests/unit/data/test_batch_edge_cases.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,5 +965,109 @@ def test_batch_boundary_in_content_type(self):
965965
self.assertIn("batch_", ct)
966966

967967

968+
# ---------------------------------------------------------------------------
969+
# 16. Robustness: malformed inputs and edge cases
970+
# ---------------------------------------------------------------------------
971+
972+
973+
class TestRobustnessEdgeCases(unittest.TestCase):
974+
"""Edge cases for input validation and malformed data handling."""
975+
976+
def test_parse_response_with_malformed_json_body(self):
977+
"""A response with invalid JSON in the body should not crash parsing."""
978+
text = "HTTP/1.1 200 OK\r\n" "Content-Type: application/json\r\n" "\r\n" "{this is not valid json}"
979+
item = _parse_http_response_part(text, content_id=None)
980+
self.assertIsNotNone(item)
981+
self.assertEqual(item.status_code, 200)
982+
# Malformed JSON: data should be None (parsing failed silently)
983+
self.assertIsNone(item.data)
984+
985+
def test_parse_response_with_truncated_json(self):
986+
"""Truncated JSON body should not crash."""
987+
text = 'HTTP/1.1 200 OK\r\n\r\n{"name": "Contoso", "accou'
988+
item = _parse_http_response_part(text, content_id=None)
989+
self.assertIsNotNone(item)
990+
self.assertEqual(item.status_code, 200)
991+
self.assertIsNone(item.data)
992+
993+
def test_changeset_exception_in_context_manager(self):
994+
"""If user code raises inside with batch.changeset(), batch should still work."""
995+
from PowerPlatform.Dataverse.operations.batch import BatchRequest
996+
997+
client = MagicMock()
998+
batch = BatchRequest(client)
999+
1000+
# Exception inside changeset -- changeset is added to items before __enter__
1001+
try:
1002+
with batch.changeset() as cs:
1003+
cs.records.create("account", {"name": "before error"})
1004+
raise ValueError("user error")
1005+
except ValueError:
1006+
pass
1007+
1008+
# The changeset IS in the items list (added in changeset() call)
1009+
# This is correct -- the changeset has 1 create operation
1010+
self.assertEqual(len(batch._items), 1)
1011+
1012+
def test_empty_string_table_name_in_create(self):
1013+
"""Empty table name should propagate (validated downstream by OData layer)."""
1014+
from PowerPlatform.Dataverse.operations.batch import BatchRequest
1015+
1016+
client = MagicMock()
1017+
batch = BatchRequest(client)
1018+
# Empty string is accepted at batch level -- validated at execute time
1019+
batch.records.create("", {"name": "test"})
1020+
self.assertEqual(len(batch._items), 1)
1021+
self.assertEqual(batch._items[0].table, "")
1022+
1023+
def test_special_chars_in_odata_filter_are_escaped(self):
1024+
"""OData filter values with single quotes are escaped by _escape_odata_quotes."""
1025+
from PowerPlatform.Dataverse.data._odata import _ODataClient
1026+
1027+
mock_auth = MagicMock()
1028+
mock_auth._acquire_token.return_value = MagicMock(access_token="token")
1029+
od = _ODataClient(mock_auth, "https://example.crm.dynamics.com")
1030+
1031+
# _escape_odata_quotes doubles single quotes
1032+
escaped = od._escape_odata_quotes("test'table")
1033+
self.assertEqual(escaped, "test''table")
1034+
1035+
# _build_get_entity uses the escaped value
1036+
req = od._build_get_entity("test'Table")
1037+
self.assertIn("test''table", req.url)
1038+
self.assertNotIn("test'table", req.url.replace("test''table", ""))
1039+
1040+
def test_batch_item_response_with_non_dict_json_body(self):
1041+
"""JSON body that is a list (not dict) should be handled."""
1042+
text = "HTTP/1.1 200 OK\r\n\r\n[1, 2, 3]"
1043+
item = _parse_http_response_part(text, content_id=None)
1044+
self.assertIsNotNone(item)
1045+
self.assertEqual(item.status_code, 200)
1046+
# Non-dict JSON: data should be None (only dicts are captured)
1047+
self.assertIsNone(item.data)
1048+
1049+
def test_batch_response_boundary_with_special_chars(self):
1050+
"""Boundary strings with special chars should be handled."""
1051+
boundary = "batch_abc+123/xyz"
1052+
resp_text = (
1053+
f"--{boundary}\r\n"
1054+
"Content-Type: application/http\r\n"
1055+
"Content-Transfer-Encoding: binary\r\n"
1056+
"\r\n"
1057+
"HTTP/1.1 204 No Content\r\n"
1058+
"\r\n"
1059+
f"--{boundary}--\r\n"
1060+
)
1061+
mock_response = MagicMock()
1062+
mock_response.headers = {"Content-Type": f'multipart/mixed; boundary="{boundary}"'}
1063+
mock_response.text = resp_text
1064+
1065+
od = _make_od()
1066+
client = _BatchClient(od)
1067+
result = client._parse_batch_response(mock_response)
1068+
self.assertEqual(len(result.responses), 1)
1069+
self.assertTrue(result.responses[0].is_success)
1070+
1071+
9681072
if __name__ == "__main__":
9691073
unittest.main()

0 commit comments

Comments
 (0)