Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Changelog
[0.2.7] - Unreleased
--------------------

Fixed
^^^^^
- Fix ``ref``/``value`` consistency for nested complex attributes like ``Manager``. :pr:`47`

Added
^^^^^
- Attribute filtering compliance checkers for ``attributes`` and ``excludedAttributes`` on single resource, list, and ``.search`` endpoints (:rfc:`7644` §3.4). :issue:`20`
Expand Down
47 changes: 26 additions & 21 deletions scim2_tester/filling.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import Any

from pydantic import Base64Bytes
from pydantic import BaseModel
from scim2_models import ComplexAttribute
from scim2_models import Extension
from scim2_models import Mutability
Expand Down Expand Up @@ -134,7 +135,7 @@ def generate_random_value(
if path.is_multivalued:
value = [value]

return fix_reference_values_in_value(value)
return value


def fill_with_random_values(
Expand Down Expand Up @@ -174,33 +175,37 @@ def fill_with_random_values(
path.set(obj, value, strict=False)

fix_primary_attributes(obj)
fix_reference_values(obj)

return obj


def fix_reference_values_in_value(value: Any) -> Any:
"""Fix reference values in any value to extract IDs from reference URLs.
def fix_reference_values(obj: BaseModel) -> None:
"""Recursively fix ref/value consistency on an object and its children.

For SCIM reference fields, correctly sets the value field to match
the ID extracted from the reference URL. Works with both single values
and lists containing reference objects.
Walks the object tree and ensures that for any object with both
``ref`` and ``value`` attributes, ``value`` matches the last segment
of the ``ref`` URL.
"""
if isinstance(value, list):
for item in value:
if (
hasattr(item, "ref")
and hasattr(item, "value")
and getattr(item, "ref", None)
):
item.value = item.ref.rsplit("/", 1)[-1]
elif (
hasattr(value, "ref")
and hasattr(value, "value")
and getattr(value, "ref", None)
):
value.value = value.ref.rsplit("/", 1)[-1]
for field_name in type(obj).model_fields:
child = getattr(obj, field_name, None)
if child is None:
continue

return value
if isinstance(child, list):
for item in child:
_fix_ref_value(item)
if isinstance(item, BaseModel):
fix_reference_values(item)
elif isinstance(child, BaseModel):
_fix_ref_value(child)
fix_reference_values(child)


def _fix_ref_value(obj: Any) -> None:
"""Set ``value`` to the last segment of ``ref`` if both attributes exist."""
if hasattr(obj, "ref") and hasattr(obj, "value") and getattr(obj, "ref", None):
obj.value = obj.ref.rsplit("/", 1)[-1]


def fix_primary_attributes(obj: Resource[Any]) -> None:
Expand Down
67 changes: 67 additions & 0 deletions tests/test_filling.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from scim2_models.resources.resource import Resource
from scim2_models.resources.user import X509Certificate

from scim2_tester.filling import _fix_ref_value
from scim2_tester.filling import fill_with_random_values
from scim2_tester.filling import fix_primary_attributes
from scim2_tester.filling import fix_reference_values
from scim2_tester.filling import generate_random_value
from scim2_tester.filling import get_model_from_ref_type
from scim2_tester.filling import get_random_example_value
Expand Down Expand Up @@ -282,3 +284,68 @@ def test_generate_random_value_required_filter(testing_context):
testing_context, Path[User]("userName"), required=[Required.true]
)
assert result


def test_fix_reference_values_in_nested_complex_attribute(testing_context, httpserver):
"""Ensures ref and value are consistent for nested complex attributes like Manager."""
user_data = {
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "manager-id",
"userName": "manager-user",
"meta": {
"resourceType": "User",
"location": f"http://localhost:{httpserver.port}/Users/manager-id",
},
}
httpserver.expect_request("/Users", method="POST").respond_with_json(
user_data, status=201
)

manager_urn = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager"
enterprise_user = User[EnterpriseUser](user_name="test")
filled = fill_with_random_values(
testing_context,
enterprise_user,
[
Path[User[EnterpriseUser]](manager_urn),
Path[User[EnterpriseUser]](f"{manager_urn}.value"),
Path[User[EnterpriseUser]](f"{manager_urn}.ref"),
],
)

manager = filled[EnterpriseUser].manager
assert manager is not None
assert manager.ref is not None
assert manager.value == manager.ref.rsplit("/", 1)[-1]


def test_fix_ref_value_on_object_with_ref_and_value():
"""Ensures _fix_ref_value corrects value from ref URL."""
from scim2_models.resources.enterprise_user import Manager

manager = Manager(
ref="http://example.com/Users/abc123",
value="wrong-value",
)
_fix_ref_value(manager)

assert manager.value == "abc123"


def test_fix_reference_values_on_list_of_members():
"""Ensures fix_reference_values fixes ref/value in list attributes."""
group = Group(display_name="test")
group.members = [
Group.Members(
ref="http://example.com/Users/user1",
value="wrong",
),
Group.Members(
ref="http://example.com/Groups/group2",
value="wrong",
),
]
fix_reference_values(group)

assert group.members[0].value == "user1"
assert group.members[1].value == "group2"
Loading