@@ -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+
9681072if __name__ == "__main__" :
9691073 unittest .main ()
0 commit comments