@@ -175,6 +175,179 @@ def test_process_migrate_resource_item_raises_on_missing_resource_data():
175175 cmd ._process_and_migrate_resource_item (resource_item , resource_context )
176176
177177
178+ @pytest .mark .django_db
179+ def test_process_resource_page_batch_raises_on_missing_resource_data ():
180+ """Test that _process_resource_page_batch raises when resource_data is missing."""
181+ cmd = MigrateCommand ()
182+ results = [{"ansible_id" : "test-id-456" , "name" : "test" }]
183+ resource_context = {"type" : Mock ()}
184+
185+ with pytest .raises (RuntimeError , match = "missing 'resource_data'" ):
186+ cmd ._process_resource_page_batch (results , resource_context )
187+
188+
189+ @pytest .mark .django_db
190+ def test_process_resource_page_batch_bulk_update ():
191+ """Test that _process_resource_page_batch calls bulk_update_resources with correct payloads."""
192+ import uuid
193+
194+ from ansible_base .resource_registry .models import ResourceType
195+
196+ cmd = MigrateCommand ()
197+ cmd .stdout = StringIO ()
198+ cmd .stderr = StringIO ()
199+ cmd .upstream_service_id = str (uuid .uuid4 ())
200+
201+ mock_client = Mock ()
202+ mock_client .service .service_cluster .service_type .name = "awx"
203+ mock_resp = Mock ()
204+ mock_resp .status_code = 200
205+ mock_resp .json .return_value = {"updated" : 2 , "errors" : []}
206+ mock_client .bulk_update_resources .return_value = mock_resp
207+ cmd .client = mock_client
208+
209+ Organization .objects .create (name = "BatchOrg1" )
210+
211+ org_resource_type = ResourceType .objects .get (name = "shared.organization" )
212+ resource_context = {
213+ "type" : org_resource_type ,
214+ "type_name" : "shared.organization" ,
215+ "type_serializer" : org_resource_type .serializer_class ,
216+ "type_name_field" : org_resource_type .get_resource_config ().name_field ,
217+ "unique_fields" : ["name" ],
218+ "LocalResourceModel" : Organization ,
219+ }
220+
221+ results = [
222+ {
223+ "ansible_id" : str (uuid .uuid4 ()),
224+ "name" : "BatchOrg1" ,
225+ "resource_type" : "shared.organization" ,
226+ "resource_data" : {"name" : "BatchOrg1" },
227+ },
228+ {
229+ "ansible_id" : str (uuid .uuid4 ()),
230+ "name" : "NewOrg" ,
231+ "resource_type" : "shared.organization" ,
232+ "resource_data" : {"name" : "NewOrg" },
233+ },
234+ ]
235+
236+ count = cmd ._process_resource_page_batch (results , resource_context )
237+ assert count == 2
238+
239+ mock_client .bulk_update_resources .assert_called_once ()
240+ bulk_items = mock_client .bulk_update_resources .call_args [0 ][0 ]
241+ assert len (bulk_items ) == 2
242+ assert all ("ansible_id" in item for item in bulk_items )
243+ # The first item (BatchOrg1 exists) triggers reconcile which sets ansible_id and resource_data
244+ merged_item = bulk_items [0 ]
245+ assert "new_ansible_id" in merged_item
246+ assert "resource_data" in merged_item
247+ # The second item (NewOrg is new) only gets service_id
248+ new_item = bulk_items [1 ]
249+ assert "service_id" in new_item
250+
251+
252+ @pytest .mark .django_db
253+ def test_process_resource_page_batch_with_partially_migrated ():
254+ """Test that is_partially_migrated is included in bulk payload when set."""
255+ import uuid
256+ from unittest .mock import patch as mock_patch
257+
258+ from ansible_base .resource_registry .models import ResourceType
259+
260+ cmd = MigrateCommand ()
261+ cmd .stdout = StringIO ()
262+ cmd .stderr = StringIO ()
263+ cmd .upstream_service_id = str (uuid .uuid4 ())
264+
265+ mock_client = Mock ()
266+ mock_client .service .service_cluster .service_type .name = "awx"
267+ mock_resp = Mock ()
268+ mock_resp .status_code = 200
269+ mock_client .bulk_update_resources .return_value = mock_resp
270+ cmd .client = mock_client
271+
272+ org_resource_type = ResourceType .objects .get (name = "shared.organization" )
273+ resource_context = {
274+ "type" : org_resource_type ,
275+ "type_name" : "shared.organization" ,
276+ "type_serializer" : org_resource_type .serializer_class ,
277+ "type_name_field" : org_resource_type .get_resource_config ().name_field ,
278+ "unique_fields" : ["name" ],
279+ "LocalResourceModel" : Organization ,
280+ }
281+
282+ results = [
283+ {
284+ "ansible_id" : str (uuid .uuid4 ()),
285+ "name" : "PartialOrg" ,
286+ "resource_type" : "shared.organization" ,
287+ "resource_data" : {"name" : "PartialOrg" },
288+ },
289+ ]
290+
291+ # Mock _reconcile_existing_resource to inject is_partially_migrated
292+ def mock_reconcile (upstream_resource , ctx , validated_data , updated_service_resource ):
293+ updated_service_resource ["is_partially_migrated" ] = True
294+ return True
295+
296+ with mock_patch .object (cmd , "_reconcile_existing_resource" , side_effect = mock_reconcile ):
297+ count = cmd ._process_resource_page_batch (results , resource_context )
298+
299+ assert count == 1
300+ bulk_items = mock_client .bulk_update_resources .call_args [0 ][0 ]
301+ assert bulk_items [0 ]["is_partially_migrated" ] is True
302+
303+
304+ @pytest .mark .django_db
305+ def test_process_resource_page_batch_rollback_on_bulk_failure ():
306+ """Test that local DB changes are rolled back if bulk_update_resources fails."""
307+ import uuid
308+
309+ from ansible_base .resource_registry .models import ResourceType
310+
311+ cmd = MigrateCommand ()
312+ cmd .stdout = StringIO ()
313+ cmd .stderr = StringIO ()
314+ cmd .upstream_service_id = str (uuid .uuid4 ())
315+
316+ mock_client = Mock ()
317+ mock_client .service .service_cluster .service_type .name = "awx"
318+ mock_resp = Mock ()
319+ mock_resp .status_code = 500
320+ mock_resp .text = "Internal Server Error"
321+ mock_client .bulk_update_resources .return_value = mock_resp
322+ cmd .client = mock_client
323+
324+ org_resource_type = ResourceType .objects .get (name = "shared.organization" )
325+ resource_context = {
326+ "type" : org_resource_type ,
327+ "type_name" : "shared.organization" ,
328+ "type_serializer" : org_resource_type .serializer_class ,
329+ "type_name_field" : org_resource_type .get_resource_config ().name_field ,
330+ "unique_fields" : ["name" ],
331+ "LocalResourceModel" : Organization ,
332+ }
333+
334+ results = [
335+ {
336+ "ansible_id" : str (uuid .uuid4 ()),
337+ "name" : "RollbackOrg" ,
338+ "resource_type" : "shared.organization" ,
339+ "resource_data" : {"name" : "RollbackOrg" },
340+ },
341+ ]
342+
343+ org_count_before = Organization .objects .count ()
344+ with pytest .raises (RuntimeError , match = "Bulk resource update failed" ):
345+ cmd ._process_resource_page_batch (results , resource_context )
346+
347+ # Verify rollback: no new org should have been created
348+ assert Organization .objects .count () == org_count_before
349+
350+
178351@pytest .mark .django_db
179352def test_reconcile_existing_resource_matching_ansible_id_same_data ():
180353 """Case 1 with matching data: logs 'Correcting service_id'."""
0 commit comments