From c776720479ccb888c159a15e1f453d4edc4d367b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Thu, 22 Jan 2026 09:18:31 +0100 Subject: [PATCH] fix: requesting a given attribute includes its sub-attributes --- doc/changelog.rst | 4 ++++ scim2_models/base.py | 25 ++++++++++++++-------- tests/test_model_attributes.py | 35 +++++++++++++++++++++++++++++++ tests/test_model_serialization.py | 1 + 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 32e1ea9..84c0e8a 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -9,6 +9,10 @@ Added - Support for Python 3.14. - Compile regexes. +Fixed +^^^^^ +- Sub-attributes of requested complex attributes are now included in responses. :issue:`114` + Removed ^^^^^^^ - Support for Python 3.9. diff --git a/scim2_models/base.py b/scim2_models/base.py index 75f62d2..479ebe7 100644 --- a/scim2_models/base.py +++ b/scim2_models/base.py @@ -29,12 +29,21 @@ from scim2_models.utils import _to_camel -def _contains_attribute_or_subattributes( - attribute_urns: list[str], attribute_urn: str -) -> bool: - return attribute_urn in attribute_urns or any( - item.startswith(f"{attribute_urn}.") or item.startswith(f"{attribute_urn}:") - for item in attribute_urns +def _is_attribute_requested(requested_urns: list[str], current_urn: str) -> bool: + """Check if an attribute should be included based on the requested URNs. + + Returns True if: + - The current attribute is explicitly requested + - A sub-attribute of the current attribute is requested + - The current attribute is a sub-attribute of a requested attribute + """ + return ( + current_urn in requested_urns + or any( + item.startswith(f"{current_urn}.") or item.startswith(f"{current_urn}:") + for item in requested_urns + ) + or any(current_urn.startswith(f"{item}.") for item in requested_urns) ) @@ -478,9 +487,7 @@ def _scim_response_serializer( if returnability == Returned.default and ( ( included_urns - and not _contains_attribute_or_subattributes( - included_urns, attribute_urn - ) + and not _is_attribute_requested(included_urns, attribute_urn) ) or attribute_urn in excluded_urns ): diff --git a/tests/test_model_attributes.py b/tests/test_model_attributes.py index 581ca45..2ac8223 100644 --- a/tests/test_model_attributes.py +++ b/tests/test_model_attributes.py @@ -10,6 +10,8 @@ from scim2_models.messages.patch_op import PatchOp from scim2_models.reference import Reference from scim2_models.resources.enterprise_user import EnterpriseUser +from scim2_models.resources.group import Group +from scim2_models.resources.group import GroupMember from scim2_models.resources.resource import Extension from scim2_models.resources.resource import Meta from scim2_models.resources.resource import Resource @@ -354,3 +356,36 @@ def test_patch_op_preserves_case_in_sub_value_fields(): value = result["Operations"][0]["value"] assert value["name"]["givenName"] == "John" + + +def test_complex_attribute_inclusion_includes_sub_attributes(): + """When a complex attribute is requested, its sub-attributes should be included.""" + user = User( + user_name="bjensen", + name={"given_name": "Barbara", "family_name": "Jensen"}, + ) + result = user.model_dump( + scim_ctx=Context.RESOURCE_QUERY_RESPONSE, + attributes=["name"], + ) + assert result["name"] == {"givenName": "Barbara", "familyName": "Jensen"} + + +def test_multivalued_complex_attribute_inclusion_includes_sub_attributes(): + """When a multi-valued complex attribute is requested, its sub-attributes should be included.""" + group = Group( + id="group-123", + display_name="Engineering", + members=[ + GroupMember(value="user-1", type="User"), + GroupMember(value="user-2", type="User"), + ], + ) + result = group.model_dump( + scim_ctx=Context.RESOURCE_QUERY_RESPONSE, + attributes=["members"], + ) + assert result["members"] == [ + {"value": "user-1", "type": "User"}, + {"value": "user-2", "type": "User"}, + ] diff --git a/tests/test_model_serialization.py b/tests/test_model_serialization.py index f888485..12c451b 100644 --- a/tests/test_model_serialization.py +++ b/tests/test_model_serialization.py @@ -327,6 +327,7 @@ def test_dump_response(context, ret_resource): "alwaysReturned": "x", "sub": { "alwaysReturned": "x", + "defaultReturned": "x", }, }