55from django .test .utils import override_settings
66
77from ansible_base .lib .utils .response import get_relative_url
8- from ansible_base .rbac .models import RoleDefinition , RoleUserAssignment
8+ from ansible_base .rbac .models import RoleDefinition , RoleTeamAssignment , RoleUserAssignment
99
1010
1111@pytest .mark .django_db
@@ -203,8 +203,8 @@ def test_role_definitions_post_disabled_by_settings(admin_api_client):
203203 (400 , 400 ), # ValidationError
204204 (401 , 403 ), # PermissionDenied
205205 (403 , 403 ), # PermissionDenied
206- (500 , 500 ), # Re-raised HTTPError → Internal Server Error
207- (None , 500 ), # HTTPError with no response → Internal Server Error
206+ (500 , 500 ), # Re-raised original HTTPError preserves stack trace
207+ (None , 500 ), # Re-raised HTTPError with no response preserves context
208208 ],
209209)
210210@pytest .mark .django_db
@@ -228,9 +228,16 @@ def test_user_assignment_remote_sync_error_handling(admin_api_client, inv_rd, ra
228228 with patch ('ansible_base.rbac.api.views.BaseAssignmentViewSet.remote_sync_assignment' ) as mock_sync :
229229 mock_sync .side_effect = http_error
230230
231- response = admin_api_client .post (url , data = data , format = "json" )
232- assert response .status_code == expected_status
233- assert 'detail' in response .data
231+ if sync_error_status in [500 , None ]:
232+ # For 500 errors and HTTPError with no response, we re-raise the original HTTPError
233+ # which will be an unhandled exception in the test client
234+ with pytest .raises (requests .exceptions .HTTPError ):
235+ admin_api_client .post (url , data = data , format = "json" )
236+ else :
237+ # For 400/401/403 errors, we convert to DRF exceptions with proper HTTP responses
238+ response = admin_api_client .post (url , data = data , format = "json" )
239+ assert response .status_code == expected_status
240+ assert 'detail' in response .data
234241
235242 # Verify that no assignment was created due to transaction rollback
236243 assert RoleUserAssignment .objects .count () == initial_count
@@ -260,3 +267,75 @@ def test_user_assignment_remote_sync_connection_error(admin_api_client, inv_rd,
260267 # Verify that no assignment was created due to transaction rollback
261268 # This confirms that our transaction.atomic() fix works correctly
262269 assert RoleUserAssignment .objects .count () == initial_count
270+
271+
272+ @pytest .mark .parametrize (
273+ "sync_error_status, expected_status" ,
274+ [
275+ (400 , 400 ), # ValidationError
276+ (401 , 403 ), # PermissionDenied
277+ (403 , 403 ), # PermissionDenied
278+ (500 , 500 ), # Re-raised original HTTPError preserves stack trace
279+ (None , 500 ), # Re-raised HTTPError with no response preserves context
280+ ],
281+ )
282+ @pytest .mark .django_db
283+ def test_team_assignment_remote_sync_error_handling (admin_api_client , inv_rd , team , inventory , sync_error_status , expected_status ):
284+ """Test that remote sync HTTP errors are handled correctly for team assignments"""
285+ url = get_relative_url ('roleteamassignment-list' )
286+ data = dict (role_definition = inv_rd .id , team = team .id , object_id = inventory .id )
287+
288+ # Track initial count to verify transaction rollback
289+ initial_count = RoleTeamAssignment .objects .count ()
290+
291+ # Mock the remote sync to raise HTTPError with specified status
292+ if sync_error_status is not None :
293+ mock_response = requests .Response ()
294+ mock_response .status_code = sync_error_status
295+ http_error = requests .exceptions .HTTPError (response = mock_response )
296+ else :
297+ # Simulate HTTPError with no response (e.g., connection failed)
298+ http_error = requests .exceptions .HTTPError (response = None )
299+
300+ with patch ('ansible_base.rbac.api.views.BaseAssignmentViewSet.remote_sync_assignment' ) as mock_sync :
301+ mock_sync .side_effect = http_error
302+
303+ if sync_error_status in [500 , None ]:
304+ # For 500 errors and HTTPError with no response, we re-raise the original HTTPError
305+ # which will be an unhandled exception in the test client
306+ with pytest .raises (requests .exceptions .HTTPError ):
307+ admin_api_client .post (url , data = data , format = "json" )
308+ else :
309+ # For 400/401/403 errors, we convert to DRF exceptions with proper HTTP responses
310+ response = admin_api_client .post (url , data = data , format = "json" )
311+ assert response .status_code == expected_status
312+ assert 'detail' in response .data
313+
314+ # Verify that no assignment was created due to transaction rollback
315+ assert RoleTeamAssignment .objects .count () == initial_count
316+
317+
318+ @pytest .mark .django_db
319+ def test_team_assignment_remote_sync_connection_error (admin_api_client , inv_rd , team , inventory ):
320+ """Test that connection errors during remote sync cause unhandled exceptions but transaction rollback works"""
321+ url = get_relative_url ('roleteamassignment-list' )
322+ data = dict (role_definition = inv_rd .id , team = team .id , object_id = inventory .id )
323+
324+ # Track initial count to verify transaction rollback
325+ initial_count = RoleTeamAssignment .objects .count ()
326+
327+ # Mock the remote sync to raise ConnectionError (no HTTP response)
328+ connection_error = requests .exceptions .ConnectionError ("Connection failed" )
329+
330+ with patch ('ansible_base.rbac.api.views.BaseAssignmentViewSet.remote_sync_assignment' ) as mock_sync :
331+ mock_sync .side_effect = connection_error
332+
333+ # ConnectionError is not currently caught, so it escapes as an unhandled exception
334+ # The Django test client converts this to a 500 response, but we can also test
335+ # by catching the exception directly to verify the rollback behavior
336+ with pytest .raises (requests .exceptions .ConnectionError ):
337+ admin_api_client .post (url , data = data , format = "json" )
338+
339+ # Verify that no assignment was created due to transaction rollback
340+ # This confirms that our transaction.atomic() fix works correctly
341+ assert RoleTeamAssignment .objects .count () == initial_count
0 commit comments