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