Skip to content

Commit 45e3d14

Browse files
haggqvistazmeuk
andauthored
fix: reference consistency for nested fields (#47)
fix: reference consistency for nested fields --------- Co-authored-by: Éloi Rivard <eloi@yaal.coop>
1 parent bdefc09 commit 45e3d14

File tree

3 files changed

+97
-21
lines changed

3 files changed

+97
-21
lines changed

doc/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changelog
44
[0.2.7] - Unreleased
55
--------------------
66

7+
Fixed
8+
^^^^^
9+
- Fix ``ref``/``value`` consistency for nested complex attributes like ``Manager``. :pr:`47`
10+
711
Added
812
^^^^^
913
- Attribute filtering compliance checkers for ``attributes`` and ``excludedAttributes`` on single resource, list, and ``.search`` endpoints (:rfc:`7644` §3.4). :issue:`20`

scim2_tester/filling.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Any
88

99
from pydantic import Base64Bytes
10+
from pydantic import BaseModel
1011
from scim2_models import ComplexAttribute
1112
from scim2_models import Extension
1213
from scim2_models import Mutability
@@ -134,7 +135,7 @@ def generate_random_value(
134135
if path.is_multivalued:
135136
value = [value]
136137

137-
return fix_reference_values_in_value(value)
138+
return value
138139

139140

140141
def fill_with_random_values(
@@ -174,33 +175,37 @@ def fill_with_random_values(
174175
path.set(obj, value, strict=False)
175176

176177
fix_primary_attributes(obj)
178+
fix_reference_values(obj)
177179

178180
return obj
179181

180182

181-
def fix_reference_values_in_value(value: Any) -> Any:
182-
"""Fix reference values in any value to extract IDs from reference URLs.
183+
def fix_reference_values(obj: BaseModel) -> None:
184+
"""Recursively fix ref/value consistency on an object and its children.
183185
184-
For SCIM reference fields, correctly sets the value field to match
185-
the ID extracted from the reference URL. Works with both single values
186-
and lists containing reference objects.
186+
Walks the object tree and ensures that for any object with both
187+
``ref`` and ``value`` attributes, ``value`` matches the last segment
188+
of the ``ref`` URL.
187189
"""
188-
if isinstance(value, list):
189-
for item in value:
190-
if (
191-
hasattr(item, "ref")
192-
and hasattr(item, "value")
193-
and getattr(item, "ref", None)
194-
):
195-
item.value = item.ref.rsplit("/", 1)[-1]
196-
elif (
197-
hasattr(value, "ref")
198-
and hasattr(value, "value")
199-
and getattr(value, "ref", None)
200-
):
201-
value.value = value.ref.rsplit("/", 1)[-1]
190+
for field_name in type(obj).model_fields:
191+
child = getattr(obj, field_name, None)
192+
if child is None:
193+
continue
202194

203-
return value
195+
if isinstance(child, list):
196+
for item in child:
197+
_fix_ref_value(item)
198+
if isinstance(item, BaseModel):
199+
fix_reference_values(item)
200+
elif isinstance(child, BaseModel):
201+
_fix_ref_value(child)
202+
fix_reference_values(child)
203+
204+
205+
def _fix_ref_value(obj: Any) -> None:
206+
"""Set ``value`` to the last segment of ``ref`` if both attributes exist."""
207+
if hasattr(obj, "ref") and hasattr(obj, "value") and getattr(obj, "ref", None):
208+
obj.value = obj.ref.rsplit("/", 1)[-1]
204209

205210

206211
def fix_primary_attributes(obj: Resource[Any]) -> None:

tests/test_filling.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
from scim2_models.resources.resource import Resource
1818
from scim2_models.resources.user import X509Certificate
1919

20+
from scim2_tester.filling import _fix_ref_value
2021
from scim2_tester.filling import fill_with_random_values
2122
from scim2_tester.filling import fix_primary_attributes
23+
from scim2_tester.filling import fix_reference_values
2224
from scim2_tester.filling import generate_random_value
2325
from scim2_tester.filling import get_model_from_ref_type
2426
from scim2_tester.filling import get_random_example_value
@@ -282,3 +284,68 @@ def test_generate_random_value_required_filter(testing_context):
282284
testing_context, Path[User]("userName"), required=[Required.true]
283285
)
284286
assert result
287+
288+
289+
def test_fix_reference_values_in_nested_complex_attribute(testing_context, httpserver):
290+
"""Ensures ref and value are consistent for nested complex attributes like Manager."""
291+
user_data = {
292+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
293+
"id": "manager-id",
294+
"userName": "manager-user",
295+
"meta": {
296+
"resourceType": "User",
297+
"location": f"http://localhost:{httpserver.port}/Users/manager-id",
298+
},
299+
}
300+
httpserver.expect_request("/Users", method="POST").respond_with_json(
301+
user_data, status=201
302+
)
303+
304+
manager_urn = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager"
305+
enterprise_user = User[EnterpriseUser](user_name="test")
306+
filled = fill_with_random_values(
307+
testing_context,
308+
enterprise_user,
309+
[
310+
Path[User[EnterpriseUser]](manager_urn),
311+
Path[User[EnterpriseUser]](f"{manager_urn}.value"),
312+
Path[User[EnterpriseUser]](f"{manager_urn}.ref"),
313+
],
314+
)
315+
316+
manager = filled[EnterpriseUser].manager
317+
assert manager is not None
318+
assert manager.ref is not None
319+
assert manager.value == manager.ref.rsplit("/", 1)[-1]
320+
321+
322+
def test_fix_ref_value_on_object_with_ref_and_value():
323+
"""Ensures _fix_ref_value corrects value from ref URL."""
324+
from scim2_models.resources.enterprise_user import Manager
325+
326+
manager = Manager(
327+
ref="http://example.com/Users/abc123",
328+
value="wrong-value",
329+
)
330+
_fix_ref_value(manager)
331+
332+
assert manager.value == "abc123"
333+
334+
335+
def test_fix_reference_values_on_list_of_members():
336+
"""Ensures fix_reference_values fixes ref/value in list attributes."""
337+
group = Group(display_name="test")
338+
group.members = [
339+
Group.Members(
340+
ref="http://example.com/Users/user1",
341+
value="wrong",
342+
),
343+
Group.Members(
344+
ref="http://example.com/Groups/group2",
345+
value="wrong",
346+
),
347+
]
348+
fix_reference_values(group)
349+
350+
assert group.members[0].value == "user1"
351+
assert group.members[1].value == "group2"

0 commit comments

Comments
 (0)