@@ -493,6 +493,114 @@ def test_serializer_allows_null_values_in_validation(self, admin_api_client, ran
493493 assert 'created_by' not in validated_data or validated_data .get ('created_by' ) is None
494494
495495
496+ @pytest .mark .django_db
497+ class TestRestClientSyncAssignment :
498+ """
499+ Test that rest_client.sync_assignment only sends the appropriate ID field.
500+
501+ For objects registered with resource registry: send only object_ansible_id
502+ For objects NOT registered: send only object_id
503+
504+ Generated by Claude Sonnet 4.5
505+ """
506+
507+ def test_sync_assignment_sends_only_object_ansible_id_for_registered_objects (self , rando , organization , org_admin_rd , mocker ):
508+ """Test that sync_assignment removes object_id when object_ansible_id is present"""
509+ from ansible_base .resource_registry .rest_client import ResourceAPIClient
510+
511+ # Create an assignment to an organization (which has a resource)
512+ assignment = org_admin_rd .give_permission (rando , organization )
513+
514+ # Create a mock client
515+ client = ResourceAPIClient (service_url = 'http://example.com' , service_path = '/api/v1/service-index/' )
516+
517+ # Mock the _sync_assignment method to capture what data is sent
518+ mock_sync = mocker .patch .object (client , '_sync_assignment' )
519+
520+ # Call sync_assignment
521+ client .sync_assignment (assignment )
522+
523+ # Verify _sync_assignment was called
524+ assert mock_sync .called
525+ sent_data = mock_sync .call_args [0 ][0 ]
526+
527+ # Should have object_ansible_id
528+ assert 'object_ansible_id' in sent_data
529+ assert sent_data ['object_ansible_id' ] == str (organization .resource .ansible_id )
530+
531+ # Should NOT have object_id (removed by sync_assignment)
532+ assert 'object_id' not in sent_data , "object_id should not be sent for registered objects"
533+
534+ def test_sync_assignment_sends_only_object_id_for_non_registered_objects (self , rando , inventory , inv_rd , mocker ):
535+ """Test that sync_assignment keeps object_id when object_ansible_id is None"""
536+ from ansible_base .resource_registry .rest_client import ResourceAPIClient
537+
538+ # Create an assignment to an inventory (which doesn't have a resource)
539+ assignment = inv_rd .give_permission (rando , inventory )
540+
541+ # Create a mock client
542+ client = ResourceAPIClient (service_url = 'http://example.com' , service_path = '/api/v1/service-index/' )
543+
544+ # Mock the _sync_assignment method to capture what data is sent
545+ mock_sync = mocker .patch .object (client , '_sync_assignment' )
546+
547+ # Call sync_assignment
548+ client .sync_assignment (assignment )
549+
550+ # Verify _sync_assignment was called
551+ assert mock_sync .called
552+ sent_data = mock_sync .call_args [0 ][0 ]
553+
554+ # Should have object_id
555+ assert 'object_id' in sent_data
556+ assert sent_data ['object_id' ] == str (inventory .id )
557+
558+ # object_ansible_id should either be absent or None
559+ assert sent_data .get ('object_ansible_id' ) is None
560+
561+
562+ @pytest .mark .django_db
563+ class TestObjectIdVsAnsibleId :
564+ """
565+ Test server-side defensive behavior: object_ansible_id takes precedence when both are provided.
566+
567+ This is a defensive measure for the edge case where a client incorrectly sends both fields
568+ that point to different objects. In this case, object_ansible_id should win.
569+
570+ Generated by Claude Sonnet 4.5
571+ """
572+
573+ def test_object_ansible_id_takes_precedence_when_both_differ (self , admin_api_client , rando , org_admin_rd , organization ):
574+ """
575+ Test DESIRED defensive server-side behavior: object_ansible_id wins when both are provided but differ.
576+
577+ This should not happen in normal operation (the client should only send one), but if it does,
578+ object_ansible_id should take precedence since it's the canonical identifier for cross-service sync.
579+ """
580+ from test_app .models import Organization
581+
582+ # Create a second organization
583+ org2 = Organization .objects .create (name = 'Other Organization' )
584+
585+ url = get_relative_url ('serviceuserassignment-assign' )
586+
587+ # Provide both with valid but different values (simulating a buggy client)
588+ # object_id points to org2, object_ansible_id points to organization
589+ data = {
590+ "role_definition" : org_admin_rd .name ,
591+ "user_ansible_id" : str (rando .resource .ansible_id ),
592+ "object_id" : str (org2 .id ), # Wrong object
593+ "object_ansible_id" : str (organization .resource .ansible_id ), # Should take precedence
594+ }
595+
596+ response = admin_api_client .post (url , data = data )
597+ assert response .status_code == 201 , f"Expected 201 but got { response .status_code } : { response .data } "
598+
599+ # Verify the assignment was made to organization (from object_ansible_id), not org2
600+ assert rando .has_obj_perm (organization , 'change' ), "object_ansible_id should take precedence"
601+ assert not rando .has_obj_perm (org2 , 'change' ), "object_id should be ignored when both provided"
602+
603+
496604@pytest .mark .django_db
497605class TestValidationErrors :
498606 """Test validation error cases in service API serializers"""
0 commit comments