diff --git a/doc/changelog.rst b/doc/changelog.rst index 6ad2cc5..0d5a68d 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -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 -------------------- diff --git a/scim2_tester/filling.py b/scim2_tester/filling.py index 8bfdbec..8995135 100644 --- a/scim2_tester/filling.py +++ b/scim2_tester/filling.py @@ -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 @@ -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) @@ -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") @@ -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) @@ -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 ( diff --git a/tests/test_filling.py b/tests/test_filling.py index 774ca36..cc12d50 100644 --- a/tests/test_filling.py +++ b/tests/test_filling.py @@ -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 @@ -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}" + ) diff --git a/tests/test_patch_remove.py b/tests/test_patch_remove.py index 7a43713..3be3cc6 100644 --- a/tests/test_patch_remove.py +++ b/tests/test_patch_remove.py @@ -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),