@@ -54,8 +54,7 @@ def patch_handler(request):
5454 }
5555
5656 json_field = field_mapping .get (path , path )
57- if json_field in resource_state :
58- del resource_state [json_field ]
57+ del resource_state [json_field ]
5958
6059 return Response (
6160 json .dumps (resource_state ),
@@ -190,9 +189,7 @@ def patch_handler(request):
190189 operation = patch_data ["Operations" ][0 ]
191190 path = operation ["path" ]
192191
193- # Remove the field from resource state
194- if path in resource_state :
195- del resource_state [path ]
192+ del resource_state [path ]
196193
197194 return Response (
198195 json .dumps (resource_state ),
@@ -297,27 +294,16 @@ def update_patch_handler(request):
297294 operation = patch_data ["Operations" ][0 ]
298295 path = operation ["path" ]
299296
300- if path .startswith (
301- "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:"
302- ):
303- field_name = path .split (":" )[- 1 ]
304- extension_key = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
305- if (
306- extension_key in resource_state
307- and field_name in resource_state [extension_key ]
308- ):
309- del resource_state [extension_key ][field_name ]
310- else :
311- field_mapping = {
312- "display_name" : "displayName" ,
313- "nick_name" : "nickName" ,
314- "profile_url" : "profileUrl" ,
315- "user_type" : "userType" ,
316- "preferred_language" : "preferredLanguage" ,
317- }
318- json_field = field_mapping .get (path , path )
319- if json_field in resource_state :
320- del resource_state [json_field ]
297+ field_mapping = {
298+ "display_name" : "displayName" ,
299+ "nick_name" : "nickName" ,
300+ "profile_url" : "profileUrl" ,
301+ "user_type" : "userType" ,
302+ "preferred_language" : "preferredLanguage" ,
303+ }
304+ json_field = field_mapping .get (path , path )
305+ if json_field in resource_state :
306+ del resource_state [json_field ]
321307
322308 return Response (
323309 json .dumps (resource_state ),
@@ -343,3 +329,111 @@ def update_patch_handler(request):
343329 assert not unexpected , (
344330 f"All results should be SUCCESS, got: { [r .reason for r in results if r .status != Status .SUCCESS ]} "
345331 )
332+
333+
334+ def test_patch_remove_query_failure_after_patch (httpserver , testing_context ):
335+ """Test PATCH remove when query fails after successful patch."""
336+ httpserver .expect_request (uri = "/Users" , method = "POST" ).respond_with_json (
337+ {
338+ "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User" ],
339+ "id" : "123" ,
340+ "userName" : "test@example.com" ,
341+ "displayName" : "test_display" ,
342+ },
343+ status = 201 ,
344+ )
345+
346+ httpserver .expect_request (uri = "/Users/123" , method = "PATCH" ).respond_with_json (
347+ {
348+ "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User" ],
349+ "id" : "123" ,
350+ "userName" : "test@example.com" ,
351+ },
352+ status = 200 ,
353+ )
354+
355+ httpserver .expect_request (uri = "/Users/123" , method = "GET" ).respond_with_json (
356+ {
357+ "schemas" : ["urn:ietf:params:scim:api:messages:2.0:Error" ],
358+ "detail" : "Resource not found after remove" ,
359+ "status" : "404" ,
360+ },
361+ status = 404 ,
362+ )
363+
364+ httpserver .expect_request (uri = "/Users/123" , method = "DELETE" ).respond_with_data (
365+ "" , status = 204
366+ )
367+
368+ results = check_remove_attribute (testing_context , User )
369+
370+ error_results = [
371+ r
372+ for r in results
373+ if r .status == Status .ERROR
374+ and "Failed to query resource after remove" in r .reason
375+ ]
376+ assert len (error_results ) > 0
377+
378+
379+ def test_patch_remove_attribute_not_removed (testing_context ):
380+ """Test PATCH remove when modify result shows attribute was not removed."""
381+ from unittest .mock import Mock
382+
383+ mock_client = Mock ()
384+ mock_user = User (id = "123" , user_name = "test@example.com" , display_name = "persistent" )
385+ mock_client .create .return_value = mock_user
386+
387+ # Mock modify to return a resource where the attribute is still present
388+ mock_modified_user = User (
389+ id = "123" ,
390+ user_name = "test@example.com" ,
391+ display_name = "persistent" , # Still there after remove attempt
392+ )
393+ mock_client .modify .return_value = mock_modified_user
394+ mock_client .query .return_value = mock_modified_user
395+
396+ testing_context .client = mock_client
397+
398+ results = check_remove_attribute (testing_context , User )
399+
400+ error_results = [
401+ r
402+ for r in results
403+ if r .status == Status .ERROR and "did not remove attribute" in r .reason
404+ ]
405+ assert len (error_results ) > 0
406+
407+
408+ def test_patch_remove_writeonly_attribute (testing_context ):
409+ """Test PATCH remove when modify result has writeOnly attribute value."""
410+ from unittest .mock import Mock
411+
412+ mock_client = Mock ()
413+ mock_user = User (id = "123" , user_name = "test@example.com" , password = "secret123" )
414+ mock_client .create .return_value = mock_user
415+
416+ # Mock modify to return a resource with writeOnly attribute still present
417+ mock_modified_user = User (
418+ id = "123" ,
419+ user_name = "test@example.com" ,
420+ password = "secret123" , # writeOnly attribute still present
421+ )
422+ mock_client .modify .return_value = mock_modified_user
423+ mock_client .query .return_value = User (
424+ id = "123" ,
425+ user_name = "test@example.com" ,
426+ # password not returned in query (writeOnly)
427+ )
428+
429+ testing_context .client = mock_client
430+
431+ results = check_remove_attribute (testing_context , User )
432+
433+ # For writeOnly attributes, should continue to query step
434+ success_results = [
435+ r
436+ for r in results
437+ if r .status == Status .SUCCESS and "password" in r .data .get ("urn" , "" )
438+ ]
439+ assert len (success_results ) > 0
0 commit comments