@@ -786,6 +786,7 @@ def test_a_replace_may_empty_a_required_string():
786786
787787
788788def test_a_replace_may_unassign_an_optional_attribute ():
789+ """Only a required attribute is protected from becoming unassigned."""
789790 patch = PatchOp [User ].model_validate (
790791 {
791792 "schemas" : ["urn:ietf:params:scim:api:messages:2.0:PatchOp" ],
@@ -813,6 +814,7 @@ def test_an_operation_without_path_cannot_write_a_read_only_attribute():
813814
814815
815816def test_an_operation_without_path_cannot_write_a_read_only_complex_attribute ():
817+ """Naming a complex attribute is enough to refuse it, without descending into its sub-attributes."""
816818 with pytest .raises (ValidationError , match = "mutability" ):
817819 PatchOp [User ].model_validate (
818820 {
@@ -826,6 +828,7 @@ def test_an_operation_without_path_cannot_write_a_read_only_complex_attribute():
826828
827829
828830def test_an_operation_without_path_cannot_unassign_a_required_attribute ():
831+ """A null value in the ``value`` unassigns its attribute as surely as a remove does."""
829832 with pytest .raises (
830833 ValidationError , match = "required attribute cannot be unassigned"
831834 ):
@@ -839,6 +842,7 @@ def test_an_operation_without_path_cannot_unassign_a_required_attribute():
839842
840843
841844def test_an_add_without_path_cannot_write_a_read_only_attribute ():
845+ """:rfc:`RFC7644 §3.5.2.1 <7644#section-3.5.2.1>` gives ``add`` the same omitted path as ``replace``."""
842846 with pytest .raises (ValidationError , match = "mutability" ):
843847 PatchOp [User ].model_validate (
844848 {
@@ -898,3 +902,52 @@ def test_unassigning_a_required_attribute_answers_mutability():
898902 scim_ctx = Context .RESOURCE_PATCH_REQUEST ,
899903 )
900904 assert exc_info .value .errors ()[0 ]["type" ] == "scim_mutability"
905+
906+
907+ def test_an_operation_without_path_takes_a_model_as_value ():
908+ """A client building its payload in Python passes a resource, where a server parses a dict."""
909+ patch = PatchOp [User ](
910+ operations = [
911+ PatchOperation [User ](
912+ op = PatchOperation .Op .replace_ , value = User (display_name = "Babs" )
913+ )
914+ ]
915+ )
916+ user = User (id = "srv-1" , user_name = "bjensen" )
917+ assert patch .patch (user )
918+ assert user .display_name == "Babs"
919+ assert user .user_name == "bjensen"
920+
921+
922+ def test_a_model_value_answers_to_the_same_constraints_as_a_dict_one ():
923+ """The form the value takes says nothing about what the operation may write."""
924+ with pytest .raises (ValidationError , match = "mutability" ):
925+ PatchOp [User ](
926+ operations = [
927+ PatchOperation [User ](
928+ op = PatchOperation .Op .replace_ ,
929+ value = User (id = "chosen-by-the-client" ),
930+ )
931+ ]
932+ )
933+
934+
935+ def test_a_model_value_carries_the_attributes_its_payload_would ():
936+ """A null attribute is not serialized, so it is neither written nor refused.
937+
938+ Unassigning through an operation without a path takes the payload form,
939+ ``{"userName": null}``, which :rfc:`RFC7643 §2.5 <7643#section-2.5>` makes
940+ equivalent to an unassigned attribute.
941+ """
942+ patch = PatchOp [User ](
943+ operations = [
944+ PatchOperation [User ](
945+ op = PatchOperation .Op .replace_ ,
946+ value = User (user_name = None , display_name = "Babs" ),
947+ )
948+ ]
949+ )
950+ user = User (id = "srv-1" , user_name = "bjensen" )
951+ patch .patch (user )
952+ assert user .user_name == "bjensen"
953+ assert user .display_name == "Babs"
0 commit comments