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
1 change: 1 addition & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Fixed
- Error and critical check results directly raise an exception when ``raise_exceptions=True``.
- Use ``ComplexAttribute`` instead of ``MultiValuedComplexAttribute`` to generate data.
- Respect the ``primary`` attribute unique truthy value when generating random values.
- Correctly handle value generation in sub-attributes. :issue:`41`

[0.2.2] - 2025-08-13
--------------------
Expand Down
31 changes: 27 additions & 4 deletions scim2_tester/filling.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@
from scim2_tester.urns import iter_all_urns
from scim2_tester.urns import set_value_by_urn


def filter_sub_urns(parent_urn: str, allowed_urns: list[str]) -> list[str]:
"""Extract and normalize sub-URNs for a parent complex attribute.

Converts "parent.child" URNs to "child" URNs for use in the complex attribute context.
"""
prefix = f"{parent_urn}."
sub_urns = []
for urn in allowed_urns:
if urn.startswith(prefix):
sub_urn = urn.removeprefix(prefix)
sub_urns.append(sub_urn)
return sub_urns


if TYPE_CHECKING:
from scim2_tester.utils import CheckContext

Expand Down Expand Up @@ -69,6 +84,7 @@ def generate_random_value(
context: "CheckContext",
urn: str,
model: type[Resource],
allowed_urns: list[str] | None = None,
) -> Any:
field_name = _find_field_name(model, urn)
field_type = get_attribute_type_by_urn(model, urn)
Expand Down Expand Up @@ -124,10 +140,14 @@ def generate_random_value(
value = random.choice(list(field_type))

elif isclass(field_type) and issubclass(field_type, ComplexAttribute):
value = fill_complex_attribute_with_random_values(context, field_type()) # type: ignore[arg-type]
sub_urns = filter_sub_urns(urn, allowed_urns) if allowed_urns else None
value = fill_complex_attribute_with_random_values(
context, field_type(), sub_urns
) # type: ignore[arg-type]

elif isclass(field_type) and issubclass(field_type, Extension):
value = fill_with_random_values(context, field_type()) # type: ignore[arg-type]
sub_urns = filter_sub_urns(urn, allowed_urns) if allowed_urns else None
value = fill_with_random_values(context, field_type(), sub_urns) # type: ignore[arg-type]

elif field_type is Base64Bytes:
value = base64.b64encode(uuid.uuid4().bytes).decode("ascii")
Expand Down Expand Up @@ -168,7 +188,9 @@ def fill_with_random_values(
]

for urn in urns:
value = generate_random_value(context, urn=urn, model=type(obj))
value = generate_random_value(
context, urn=urn, model=type(obj), allowed_urns=urns
)
set_value_by_urn(obj, urn, value)

fix_primary_attributes(obj)
Expand All @@ -179,13 +201,14 @@ def fill_with_random_values(
def fill_complex_attribute_with_random_values(
context: "CheckContext",
obj: ComplexAttribute,
urns: list[str] | None = None,
) -> Resource[Any] | None:
"""Fill a ComplexAttribute with random values.

For SCIM reference fields, correctly sets the value field to match
the ID extracted from the reference URL.
"""
fill_with_random_values(context, obj)
fill_with_random_values(context, obj, urns)
if "ref" in type(obj).model_fields and "value" in type(obj).model_fields:
ref_type = type(obj).get_field_root_type("ref")
if (
Expand Down
25 changes: 25 additions & 0 deletions tests/test_filling.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""Test automatic field filling functionality."""

from typing import Annotated
from typing import Literal
from unittest.mock import patch

from scim2_models import Email
from scim2_models import Group
from scim2_models import Mutability
from scim2_models import PhoneNumber
from scim2_models import User
from scim2_models.attributes import ComplexAttribute
from scim2_models.resources.resource import Resource
from scim2_models.resources.user import X509Certificate

from scim2_tester.filling import fill_with_random_values
Expand Down Expand Up @@ -240,3 +244,24 @@ def test_fill_with_random_values_phone_numbers_primary_constraint(testing_contex

primary_count = sum(1 for phone in filled_user.phone_numbers if phone.primary)
assert primary_count == 1


def test_fill_with_random_values_ignores_mutability_filter(testing_context):
"""Demonstrates that complex attribute generation fills read-only sub-attributes incorrectly."""

class TestComplexAttr(ComplexAttribute):
writable_field: str | None = None
readonly_field: Annotated[str | None, Mutability.read_only] = None

class TestResource(Resource):
test_attr: TestComplexAttr | None = None

resource = TestResource(schemas=["urn:test:schema"])

filled_resource = fill_with_random_values(
testing_context, resource, ["test_attr.writable_field"]
)

assert filled_resource.test_attr.readonly_field is None, (
f"readonly_field should be None but got {filled_resource.test_attr.readonly_field}"
)
7 changes: 1 addition & 6 deletions tests/test_patch_remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,7 @@ def create_handler(request):
"userType",
"preferredLanguage",
]:
if key in request_data:
response_data[key] = request_data[key]

extension_key = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
if extension_key in request_data:
response_data[extension_key] = request_data[extension_key]
response_data[key] = request_data[key]

return Response(
json.dumps(response_data),
Expand Down
Loading