Source code for scim2_tester.checkers.patch_replace

"""PATCH replace operation checkers for SCIM compliance testing."""

from typing import Any

from scim2_client import SCIMClientError
from scim2_models import Mutability
from scim2_models import PatchOp
from scim2_models import PatchOperation
from scim2_models import Resource
from scim2_models.path import Path

from ..filling import generate_random_value
from ..utils import CheckContext
from ..utils import CheckResult
from ..utils import Status
from ..utils import check_result
from ..utils import checker
from ..utils import fields_equality


[docs] @checker("patch:replace") def check_replace_attribute( context: CheckContext, model: type[Resource[Any]] ) -> list[CheckResult]: """Test PATCH replace operation on all attributes (simple, complex, and extensions). Creates a resource with initial values, then iterates over ALL possible URNs (base model, extensions, and sub-attributes) to test PATCH replace operations systematically. Uses a unified approach that handles all attribute types consistently. **Tested Behavior:** - Replacing existing attribute values (simple, complex, and extension attributes) - Server accepts the PATCH request with correct URN paths for extensions - Response contains the replaced attribute with correct new values **Status:** - :attr:`~scim2_tester.Status.SUCCESS`: Attribute successfully replaced - :attr:`~scim2_tester.Status.ERROR`: Failed to replace attribute - :attr:`~scim2_tester.Status.SKIPPED`: No replaceable attributes found or PATCH not supported .. pull-quote:: :rfc:`RFC 7644 Section 3.5.2.3 - Replace Operation <7644#section-3.5.2.3>` "The 'replace' operation replaces the value at the target location specified by the 'path'." """ if ( context.client.service_provider_config and not context.client.service_provider_config.patch.supported ): return [ check_result( context, status=Status.SKIPPED, reason="PATCH operations not supported by server", resource_type=model.__name__, ) ] results = [] all_paths = list( Path[model].iter_paths( mutability=[Mutability.read_write, Mutability.write_only], include_subattributes=False, ) ) if not all_paths: return [ check_result( context, status=Status.SKIPPED, reason=f"No replaceable attributes found for {model.__name__}", resource_type=model.__name__, ) ] base_resource = context.resource_manager.create_and_register(model) for path in all_paths: urn = str(path) patch_value = generate_random_value(context, path=path) mutability = path.get_annotation(Mutability) patch_op = PatchOp[type(base_resource)]( operations=[ PatchOperation( op=PatchOperation.Op.replace_, path=path, value=patch_value, ) ] ) try: modify_result = context.client.modify( resource_model=type(base_resource), id=base_resource.id, patch_op=patch_op, ) except SCIMClientError as exc: results.append( check_result( context, status=Status.ERROR, reason=f"Failed to replace attribute '{urn}': {exc}", resource_type=model.__name__, data={ "urn": urn, "error": exc, "patch_value": patch_value, }, ) ) continue if modify_result is not None: modify_actual_value = path.get(modify_result) if mutability != Mutability.write_only and not fields_equality( patch_value, modify_actual_value ): results.append( check_result( context, status=Status.ERROR, reason=( f"PATCH modify() returned unexpected value for '{urn}'.\n" f"Patched value: {patch_value}\n" f"Returned value: {modify_actual_value}" ), resource_type=model.__name__, data={ "urn": urn, "expected": patch_value, "modify_actual": modify_actual_value, }, ) ) continue try: updated_resource = context.client.query( type(base_resource), base_resource.id, ) except SCIMClientError as exc: results.append( check_result( context, status=Status.ERROR, reason=f"Failed to query resource after replace on '{urn}': {exc}", resource_type=model.__name__, data={ "urn": urn, "error": exc, "patch_value": patch_value, }, ) ) continue actual_value = path.get(updated_resource) if mutability == Mutability.write_only or fields_equality( patch_value, actual_value ): results.append( check_result( context, status=Status.SUCCESS, reason=f"Successfully replaced attribute '{urn}'", resource_type=model.__name__, data={ "urn": urn, "value": patch_value, }, ) ) else: results.append( check_result( context, status=Status.ERROR, reason=( f"Attribute '{urn}' was not replaced or has unexpected value\n" f"Patched value: {patch_value}\n" f"Returned value: {actual_value}" ), resource_type=model.__name__, data={ "urn": urn, "expected": patch_value, "actual": actual_value, }, ) ) return results