@@ -605,33 +605,33 @@ def setUp(self):
605605 self .client = _ODataClient (mock_auth , "https://example.crm.dynamics.com" )
606606
607607 def _make_raw_response (self , status_code , json_data = None , headers = None ):
608- r = MagicMock ()
609- r .status_code = status_code
610- r .text = "body"
611- r .json .return_value = json_data or {}
612- r .headers = headers or {}
613- return r
608+ response = MagicMock ()
609+ response .status_code = status_code
610+ response .text = "body"
611+ response .json .return_value = json_data or {}
612+ response .headers = headers or {}
613+ return response
614614
615615 def test_message_key_fallback_used_when_no_error_key (self ):
616616 """_request uses 'message' key when 'error' key is absent."""
617- r = self ._make_raw_response (400 , json_data = {"message" : "Bad input received" })
618- self .client ._raw_request = MagicMock (return_value = r )
617+ response = self ._make_raw_response (400 , json_data = {"message" : "Bad input received" })
618+ self .client ._raw_request = MagicMock (return_value = response )
619619 with self .assertRaises (HttpError ) as ctx :
620620 self .client ._request ("get" , "http://example.com/test" )
621621 self .assertIn ("Bad input received" , str (ctx .exception ))
622622
623623 def test_retry_after_non_int_not_stored_in_details (self ):
624624 """Retry-After header that is non-numeric results in retry_after absent from details."""
625- r = self ._make_raw_response (429 , headers = {"Retry-After" : "not-a-number" })
626- self .client ._raw_request = MagicMock (return_value = r )
625+ response = self ._make_raw_response (429 , headers = {"Retry-After" : "not-a-number" })
626+ self .client ._raw_request = MagicMock (return_value = response )
627627 with self .assertRaises (HttpError ) as ctx :
628628 self .client ._request ("get" , "http://example.com/test" )
629629 self .assertIsNone (ctx .exception .details .get ("retry_after" ))
630630
631631 def test_retry_after_int_stored_in_details (self ):
632632 """Retry-After header that is numeric is stored in exception details."""
633- r = self ._make_raw_response (429 , headers = {"Retry-After" : "30" })
634- self .client ._raw_request = MagicMock (return_value = r )
633+ response = self ._make_raw_response (429 , headers = {"Retry-After" : "30" })
634+ self .client ._raw_request = MagicMock (return_value = response )
635635 with self .assertRaises (HttpError ) as ctx :
636636 self .client ._request ("get" , "http://example.com/test" )
637637 self .assertEqual (ctx .exception .details .get ("retry_after" ), 30 )
@@ -665,49 +665,62 @@ def test_odata_type_already_present_not_duplicated(self):
665665
666666 def test_body_not_dict_returns_empty_list (self ):
667667 """When response body is not a dict, returns empty list."""
668- r = _mock_response (text = '["id1", "id2"]' )
669- r .json .return_value = ["id1" , "id2" ]
670- self .od ._request .return_value = r
668+ response = _mock_response (text = '["id1", "id2"]' )
669+ response .json .return_value = ["id1" , "id2" ]
670+ self .od ._request .return_value = response
671671 result = self .od ._create_multiple ("accounts" , "account" , [{"name" : "A" }])
672672 self .assertEqual (result , [])
673673
674674 def test_value_key_path_extracts_ids (self ):
675675 """Falls back to 'value' key to extract IDs via heuristic."""
676676 long_guid = "a" * 32
677- r = _mock_response (
677+ response = _mock_response (
678678 json_data = {"value" : [{"accountid" : long_guid , "name" : "Test" }]},
679679 text = "..." ,
680680 )
681- self .od ._request .return_value = r
681+ self .od ._request .return_value = response
682682 result = self .od ._create_multiple ("accounts" , "account" , [{"name" : "Test" }])
683683 self .assertEqual (result , [long_guid ])
684684
685685 def test_value_key_with_non_dict_items_returns_empty (self ):
686686 """'value' list with non-dict items returns empty list."""
687- r = _mock_response (json_data = {"value" : ["not-a-dict" ]}, text = "..." )
688- self .od ._request .return_value = r
687+ response = _mock_response (json_data = {"value" : ["not-a-dict" ]}, text = "..." )
688+ self .od ._request .return_value = response
689689 self .od ._convert_labels_to_ints = MagicMock (side_effect = lambda _ , rec : rec )
690690 result = self .od ._create_multiple ("accounts" , "account" , [{"name" : "Test" }])
691691 self .assertEqual (result , [])
692692
693693 def test_no_ids_or_value_key_returns_empty_list (self ):
694694 """When body has neither 'Ids' nor 'value' keys, returns empty list."""
695- r = _mock_response (json_data = {"something_else" : "data" }, text = "..." )
696- self .od ._request .return_value = r
695+ response = _mock_response (json_data = {"something_else" : "data" }, text = "..." )
696+ self .od ._request .return_value = response
697697 self .od ._convert_labels_to_ints = MagicMock (side_effect = lambda _ , rec : rec )
698698 result = self .od ._create_multiple ("accounts" , "account" , [{"name" : "Test" }])
699699 self .assertEqual (result , [])
700700
701701 def test_value_parse_error_returns_empty_list (self ):
702702 """ValueError in body.json() returns empty list."""
703- r = MagicMock ()
704- r .text = "invalid json"
705- r .json .side_effect = ValueError ("bad json" )
706- self .od ._request .return_value = r
703+ response = MagicMock ()
704+ response .text = "invalid json"
705+ response .json .side_effect = ValueError ("bad json" )
706+ self .od ._request .return_value = response
707707 self .od ._convert_labels_to_ints = MagicMock (side_effect = lambda _ , rec : rec )
708708 result = self .od ._create_multiple ("accounts" , "account" , [{"name" : "Test" }])
709709 self .assertEqual (result , [])
710710
711+ def test_multiple_records_returns_all_ids (self ):
712+ """All IDs from the Ids response key are returned for multiple input records."""
713+ self .od ._request .return_value = _mock_response (
714+ json_data = {"Ids" : ["id-1" , "id-2" , "id-3" ]},
715+ text = '{"Ids": ["id-1", "id-2", "id-3"]}' ,
716+ )
717+ result = self .od ._create_multiple (
718+ "accounts" ,
719+ "account" ,
720+ [{"name" : "A" }, {"name" : "B" }, {"name" : "C" }],
721+ )
722+ self .assertEqual (result , ["id-1" , "id-2" , "id-3" ])
723+
711724
712725class TestPrimaryIdAttr (unittest .TestCase ):
713726 """Unit tests for _ODataClient._primary_id_attr cache-miss behavior."""
@@ -857,6 +870,20 @@ def test_payload_contains_targets_array(self):
857870 self .assertEqual (len (payload ["Targets" ]), 1 )
858871 self .assertIn ("@odata.type" , payload ["Targets" ][0 ])
859872
873+ def test_multiple_records_all_in_targets (self ):
874+ """All records are included in the Targets payload for multiple inputs."""
875+ self .od ._request .return_value = _mock_response ()
876+ records = [
877+ {"accountid" : "id-1" , "name" : "A" },
878+ {"accountid" : "id-2" , "name" : "B" },
879+ {"accountid" : "id-3" , "name" : "C" },
880+ ]
881+ self .od ._update_multiple ("accounts" , "account" , records )
882+ payload = json .loads (self .od ._request .call_args .kwargs ["data" ])
883+ self .assertEqual (len (payload ["Targets" ]), 3 )
884+ self .assertEqual (payload ["Targets" ][0 ]["accountid" ], "id-1" )
885+ self .assertEqual (payload ["Targets" ][2 ]["accountid" ], "id-3" )
886+
860887
861888class TestDeleteMultiple (unittest .TestCase ):
862889 """Unit tests for _ODataClient._delete_multiple."""
@@ -905,10 +932,10 @@ def test_returns_none_when_no_job_id_in_body(self):
905932
906933 def test_handles_value_error_in_json_parsing (self ):
907934 """_delete_multiple handles ValueError in response JSON parsing gracefully."""
908- r = MagicMock ()
909- r .text = "invalid"
910- r .json .side_effect = ValueError
911- self .od ._request .return_value = r
935+ response = MagicMock ()
936+ response .text = "invalid"
937+ response .json .side_effect = ValueError
938+ self .od ._request .return_value = response
912939 result = self .od ._delete_multiple ("account" , ["id-1" ])
913940 self .assertIsNone (result )
914941
@@ -944,8 +971,8 @@ def setUp(self):
944971
945972 def _single_page_response (self , items = None ):
946973 data = {"value" : items or [{"accountid" : "id-1" }]}
947- r = _mock_response (json_data = data , text = str (data ))
948- self .od ._request .return_value = r
974+ response = _mock_response (json_data = data , text = str (data ))
975+ self .od ._request .return_value = response
949976
950977 def test_filter_param_passed (self ):
951978 """_get_multiple passes $filter to params."""
@@ -999,10 +1026,10 @@ def test_page_size_sets_prefer_header(self):
9991026
10001027 def test_value_error_in_json_returns_empty (self ):
10011028 """ValueError in page JSON parsing yields nothing."""
1002- r = MagicMock ()
1003- r .text = "bad json"
1004- r .json .side_effect = ValueError
1005- self .od ._request .return_value = r
1029+ response = MagicMock ()
1030+ response .text = "bad json"
1031+ response .json .side_effect = ValueError
1032+ self .od ._request .return_value = response
10061033 pages = list (self .od ._get_multiple ("account" ))
10071034 self .assertEqual (pages , [])
10081035
@@ -1043,8 +1070,8 @@ def test_stops_when_no_nextlink(self):
10431070 def test_filters_non_dict_items_from_page (self ):
10441071 """_get_multiple filters out non-dict items from each page."""
10451072 data = {"value" : [{"accountid" : "id-1" }, "not-a-dict" , 42 ]}
1046- r = _mock_response (json_data = data , text = str (data ))
1047- self .od ._request .return_value = r
1073+ response = _mock_response (json_data = data , text = str (data ))
1074+ self .od ._request .return_value = response
10481075 pages = list (self .od ._get_multiple ("account" ))
10491076 self .assertEqual (len (pages ), 1 )
10501077 self .assertEqual (len (pages [0 ]), 1 )
@@ -1053,8 +1080,8 @@ def test_filters_non_dict_items_from_page(self):
10531080 def test_empty_value_list_yields_nothing (self ):
10541081 """_get_multiple yields nothing when value list is empty."""
10551082 data = {"value" : []}
1056- r = _mock_response (json_data = data , text = str (data ))
1057- self .od ._request .return_value = r
1083+ response = _mock_response (json_data = data , text = str (data ))
1084+ self .od ._request .return_value = response
10581085 pages = list (self .od ._get_multiple ("account" ))
10591086 self .assertEqual (pages , [])
10601087
@@ -1096,26 +1123,26 @@ def test_filters_non_dict_rows(self):
10961123
10971124 def test_body_as_list_fallback (self ):
10981125 """_query_sql handles body being a list directly."""
1099- r = _mock_response (text = "..." )
1100- r .json .return_value = [{"name" : "A" }, {"name" : "B" }]
1101- self .od ._request .return_value = r
1126+ response = _mock_response (text = "..." )
1127+ response .json .return_value = [{"name" : "A" }, {"name" : "B" }]
1128+ self .od ._request .return_value = response
11021129 result = self .od ._query_sql ("SELECT name FROM account" )
11031130 self .assertEqual (len (result ), 2 )
11041131
11051132 def test_value_error_in_json_returns_empty (self ):
11061133 """_query_sql returns empty list when JSON parsing fails."""
1107- r = MagicMock ()
1108- r .text = "bad json"
1109- r .json .side_effect = ValueError
1110- self .od ._request .return_value = r
1134+ response = MagicMock ()
1135+ response .text = "bad json"
1136+ response .json .side_effect = ValueError
1137+ self .od ._request .return_value = response
11111138 result = self .od ._query_sql ("SELECT name FROM account" )
11121139 self .assertEqual (result , [])
11131140
11141141 def test_unexpected_body_returns_empty (self ):
11151142 """_query_sql returns empty list for non-dict, non-list body."""
1116- r = _mock_response (text = "..." )
1117- r .json .return_value = "unexpected"
1118- self .od ._request .return_value = r
1143+ response = _mock_response (text = "..." )
1144+ response .json .return_value = "unexpected"
1145+ self .od ._request .return_value = response
11191146 result = self .od ._query_sql ("SELECT name FROM account" )
11201147 self .assertEqual (result , [])
11211148
@@ -1145,10 +1172,10 @@ def test_empty_table_schema_name_raises_value_error(self):
11451172
11461173 def test_json_value_error_in_response_treated_as_empty (self ):
11471174 """_entity_set_from_schema_name handles ValueError in JSON parsing."""
1148- r = MagicMock ()
1149- r .text = "invalid json"
1150- r .json .side_effect = ValueError
1151- self .od ._request .return_value = r
1175+ response = MagicMock ()
1176+ response .text = "invalid json"
1177+ response .json .side_effect = ValueError
1178+ self .od ._request .return_value = response
11521179 with self .assertRaises (MetadataError ):
11531180 self .od ._entity_set_from_schema_name ("account" )
11541181
@@ -1331,10 +1358,10 @@ def test_extra_select_skips_odata_annotation_pieces(self):
13311358
13321359 def test_value_error_in_json_returns_none (self ):
13331360 """_get_attribute_metadata returns None on JSON parse failure."""
1334- r = MagicMock ()
1335- r .text = "bad json"
1336- r .json .side_effect = ValueError
1337- self .od ._request .return_value = r
1361+ response = MagicMock ()
1362+ response .text = "bad json"
1363+ response .json .side_effect = ValueError
1364+ self .od ._request .return_value = response
13381365 result = self .od ._get_attribute_metadata ("meta-001" , "name" )
13391366 self .assertIsNone (result )
13401367
@@ -2374,11 +2401,11 @@ def test_convert_different_tables_separate_fetches(self):
23742401 resp2 = self ._bulk_response (("new_status" , [(100 , "Open" )]))
23752402 self .od ._request .side_effect = [resp1 , resp2 ]
23762403
2377- r1 = self .od ._convert_labels_to_ints ("account" , {"industrycode" : "Tech" })
2378- r2 = self .od ._convert_labels_to_ints ("new_ticket" , {"new_status" : "Open" })
2404+ result1 = self .od ._convert_labels_to_ints ("account" , {"industrycode" : "Tech" })
2405+ result2 = self .od ._convert_labels_to_ints ("new_ticket" , {"new_status" : "Open" })
23792406
2380- self .assertEqual (r1 ["industrycode" ], 6 )
2381- self .assertEqual (r2 ["new_status" ], 100 )
2407+ self .assertEqual (result1 ["industrycode" ], 6 )
2408+ self .assertEqual (result2 ["new_status" ], 100 )
23822409 self .assertEqual (self .od ._request .call_count , 2 )
23832410
23842411 def test_convert_only_odata_and_non_strings_skips_fetch (self ):
0 commit comments