@@ -643,9 +643,144 @@ def test_single_failure_makes_has_errors_true(self):
643643 self .assertTrue (result .has_errors )
644644 self .assertEqual (len (result .failed ), 1 )
645645
646+ def test_created_ids_from_create_multiple_response_body (self ):
647+ """CreateMultiple returns IDs in data['Ids'], not in entity_id header."""
648+ responses = [
649+ # CreateMultiple response: 200 OK with {"Ids": [...]} body
650+ BatchItemResponse (
651+ status_code = 200 ,
652+ entity_id = None ,
653+ data = {"Ids" : ["guid-1" , "guid-2" , "guid-3" ]},
654+ ),
655+ ]
656+ result = BatchResult (responses = responses )
657+ self .assertEqual (result .created_ids , ["guid-1" , "guid-2" , "guid-3" ])
658+
659+ def test_created_ids_combines_header_and_body_ids (self ):
660+ """created_ids includes both OData-EntityId (single create) and Ids array (bulk)."""
661+ responses = [
662+ # Single create: entity_id from header
663+ BatchItemResponse (status_code = 204 , entity_id = "single-id" ),
664+ # CreateMultiple: Ids from body
665+ BatchItemResponse (
666+ status_code = 200 ,
667+ entity_id = None ,
668+ data = {"Ids" : ["bulk-id-1" , "bulk-id-2" ]},
669+ ),
670+ ]
671+ result = BatchResult (responses = responses )
672+ self .assertEqual (result .created_ids , ["single-id" , "bulk-id-1" , "bulk-id-2" ])
673+
674+ def test_created_ids_ignores_non_string_ids_in_body (self ):
675+ """Non-string values in data['Ids'] are filtered out."""
676+ responses = [
677+ BatchItemResponse (
678+ status_code = 200 ,
679+ data = {"Ids" : ["good-id" , 12345 , None , "another-id" ]},
680+ ),
681+ ]
682+ result = BatchResult (responses = responses )
683+ self .assertEqual (result .created_ids , ["good-id" , "another-id" ])
684+
685+ def test_created_ids_skips_failed_create_multiple (self ):
686+ """Failed CreateMultiple responses should not contribute IDs."""
687+ responses = [
688+ BatchItemResponse (
689+ status_code = 400 ,
690+ data = {"error" : {"code" : "0x123" , "message" : "fail" }},
691+ ),
692+ ]
693+ result = BatchResult (responses = responses )
694+ self .assertEqual (result .created_ids , [])
695+
696+
697+ # ---------------------------------------------------------------------------
698+ # 12. CreateMultiple response parsing in batch
699+ # ---------------------------------------------------------------------------
700+
701+
702+ class TestCreateMultipleInBatch (unittest .TestCase ):
703+ """CreateMultiple action returns 200 with {Ids: [...]} in the body."""
704+
705+ def test_create_multiple_response_parsed (self ):
706+ """A 200 OK CreateMultiple response has IDs in the body, not in headers."""
707+ ids_body = json .dumps ({"Ids" : ["aaa-111" , "bbb-222" , "ccc-333" ]})
708+ batch_boundary = "batchresponse_cm123"
709+ resp_text = (
710+ f"--{ batch_boundary } \r \n "
711+ "Content-Type: application/http\r \n "
712+ "Content-Transfer-Encoding: binary\r \n "
713+ "\r \n "
714+ "HTTP/1.1 200 OK\r \n "
715+ "Content-Type: application/json; odata.metadata=minimal\r \n "
716+ "OData-Version: 4.0\r \n "
717+ "\r \n "
718+ f"{ ids_body } \r \n "
719+ f"--{ batch_boundary } --\r \n "
720+ )
721+
722+ mock_response = MagicMock ()
723+ mock_response .headers = {"Content-Type" : f'multipart/mixed; boundary="{ batch_boundary } "' }
724+ mock_response .text = resp_text
725+
726+ od = _make_od ()
727+ client = _BatchClient (od )
728+ result = client ._parse_batch_response (mock_response )
729+
730+ self .assertFalse (result .has_errors )
731+ self .assertEqual (len (result .succeeded ), 1 )
732+ # entity_id should be None (no OData-EntityId header for CreateMultiple)
733+ self .assertIsNone (result .succeeded [0 ].entity_id )
734+ # But data should contain the Ids array
735+ self .assertEqual (result .succeeded [0 ].data ["Ids" ], ["aaa-111" , "bbb-222" , "ccc-333" ])
736+ # And created_ids should extract them
737+ self .assertEqual (result .created_ids , ["aaa-111" , "bbb-222" , "ccc-333" ])
738+
739+ def test_mixed_single_and_bulk_creates (self ):
740+ """Batch with both individual POST create and CreateMultiple."""
741+ single_guid = "11111111-1111-1111-1111-111111111111"
742+ ids_body = json .dumps ({"Ids" : ["bulk-1" , "bulk-2" ]})
743+ batch_boundary = "batchresponse_mix_cm"
744+ resp_text = (
745+ # Individual create: 204 with OData-EntityId
746+ f"--{ batch_boundary } \r \n "
747+ "Content-Type: application/http\r \n "
748+ "Content-Transfer-Encoding: binary\r \n "
749+ "\r \n "
750+ "HTTP/1.1 204 No Content\r \n "
751+ "OData-Version: 4.0\r \n "
752+ f"OData-EntityId: https://org.crm.dynamics.com/api/data/v9.2/"
753+ f"accounts({ single_guid } )\r \n "
754+ "\r \n "
755+ "\r \n "
756+ # CreateMultiple: 200 with Ids body
757+ f"--{ batch_boundary } \r \n "
758+ "Content-Type: application/http\r \n "
759+ "Content-Transfer-Encoding: binary\r \n "
760+ "\r \n "
761+ "HTTP/1.1 200 OK\r \n "
762+ "Content-Type: application/json\r \n "
763+ "\r \n "
764+ f"{ ids_body } \r \n "
765+ f"--{ batch_boundary } --\r \n "
766+ )
767+
768+ mock_response = MagicMock ()
769+ mock_response .headers = {"Content-Type" : f'multipart/mixed; boundary="{ batch_boundary } "' }
770+ mock_response .text = resp_text
771+
772+ od = _make_od ()
773+ client = _BatchClient (od )
774+ result = client ._parse_batch_response (mock_response )
775+
776+ self .assertFalse (result .has_errors )
777+ self .assertEqual (len (result .succeeded ), 2 )
778+ # All 3 IDs should appear in created_ids
779+ self .assertEqual (result .created_ids , [single_guid , "bulk-1" , "bulk-2" ])
780+
646781
647782# ---------------------------------------------------------------------------
648- # 12 . Multipart parsing edge cases
783+ # 13 . Multipart parsing edge cases
649784# ---------------------------------------------------------------------------
650785
651786
0 commit comments