From 3734e6e48777068e22f4450b61f42fa3d534ebce Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Mon, 18 May 2026 10:34:50 +0530 Subject: [PATCH 1/5] feat: enhance pre-update profile action to return execution status and handle multi-valued claims --- .../scim2/common/impl/SCIMUserManager.java | 65 +++++++++++++++++-- .../PreUpdateProfileActionExecutor.java | 12 ++-- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java index 8d4e17bb4..5c9bd72eb 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java @@ -32,6 +32,7 @@ import org.json.JSONObject; import org.wso2.carbon.CarbonConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException; import org.wso2.carbon.identity.application.common.model.ServiceProvider; @@ -5896,13 +5897,44 @@ private void updateUserClaims(User user, Map oldClaimList, if (isExecutableUserProfileUpdate) { - Map multiValuedClaimsToModify = - SCIMCommonUtils.getSimpleMultiValuedClaimsToModify(oldClaimList, simpleMultiValuedClaimsToBeAdded, - simpleMultiValuedClaimsToBeRemoved); - userClaimsToBeModifiedIncludingMultiValueClaims.putAll(multiValuedClaimsToModify); + populateMultiValuedClaimsToModify(oldClaimList, simpleMultiValuedClaimsToBeAdded, + simpleMultiValuedClaimsToBeRemoved, userClaimsToBeModifiedIncludingMultiValueClaims); - preUpdateProfileActionExecutor.execute(user, userClaimsToBeModifiedIncludingMultiValueClaims, - userClaimsToBeDeleted); + ActionExecutionStatus actionExecutionStatus = preUpdateProfileActionExecutor.execute(user, + userClaimsToBeModifiedIncludingMultiValueClaims, userClaimsToBeDeleted); + if (actionExecutionStatus != null) { + + Map userClaimsToBeAddedFromAction = (HashMap) + actionExecutionStatus.getResponseContext().get("userClaimsToBeAdded"); + Map userClaimsToBeModifiedFromAction = (HashMap) + actionExecutionStatus.getResponseContext().get("userClaimsToBeModified"); + Map userClaimsToBeDeletedFromAction = (HashMap) + actionExecutionStatus.getResponseContext().get("userClaimsToBeRemoved"); + Map> simpleMultiValuedClaimsToBeAddedFromAction = (HashMap>) + actionExecutionStatus.getResponseContext().get("multiValuedClaimsToBeAdded"); + Map> simpleMultiValuedClaimsToBeRemovedFromAction = (HashMap>) + actionExecutionStatus.getResponseContext().get("multiValuedClaimsToBeRemoved"); + + if (userClaimsToBeAddedFromAction != null) { + userClaimsToBeAdded.putAll(userClaimsToBeAddedFromAction); + userClaimsToBeModified.putAll(userClaimsToBeAddedFromAction); + } + if (userClaimsToBeModifiedFromAction != null) { + userClaimsToBeModified.putAll(userClaimsToBeModifiedFromAction); + } + if (userClaimsToBeDeletedFromAction != null) { + userClaimsToBeDeleted.putAll(userClaimsToBeDeletedFromAction); + userClaimsToBeModified.keySet().removeAll(userClaimsToBeDeletedFromAction.keySet()); + } + if (simpleMultiValuedClaimsToBeAddedFromAction != null) { + simpleMultiValuedClaimsToBeAdded.putAll(simpleMultiValuedClaimsToBeAddedFromAction); + } + if (simpleMultiValuedClaimsToBeRemovedFromAction != null) { + simpleMultiValuedClaimsToBeRemoved.putAll(simpleMultiValuedClaimsToBeRemovedFromAction); + } + populateMultiValuedClaimsToModify(oldClaimList, simpleMultiValuedClaimsToBeAdded, + simpleMultiValuedClaimsToBeRemoved, userClaimsToBeModifiedIncludingMultiValueClaims); + } } if (log.isDebugEnabled()) { @@ -5948,6 +5980,27 @@ private void updateUserClaims(User user, Map oldClaimList, } } + /** + * Populate both modifiable single and multi-valued claims. + * + * @param oldClaimList User claim list for the user's existing state. + * @param simpleMultiValuedClaimsToBeAdded Map of simple multi-valued claims to be added. + * @param simpleMultiValuedClaimsToBeRemoved Map of simple multi-valued claims to be removed. + * @param userClaimsToBeModifiedIncludingMultiValueClaims Map of both single and multi valued claims to be modified. + * @return Map with the same keys and values as lists of strings, splitting multi-valued claims if necessary. + */ + private void populateMultiValuedClaimsToModify(Map oldClaimList, Map> + simpleMultiValuedClaimsToBeAdded, Map> simpleMultiValuedClaimsToBeRemoved, + Map userClaimsToBeModifiedIncludingMultiValueClaims) + { + + Map multiValuedClaimsToModify = + SCIMCommonUtils.getSimpleMultiValuedClaimsToModify(oldClaimList, simpleMultiValuedClaimsToBeAdded, + simpleMultiValuedClaimsToBeRemoved); + userClaimsToBeModifiedIncludingMultiValueClaims.putAll(multiValuedClaimsToModify); + } + /** * Convert the claim values to list of strings. * diff --git a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutor.java b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutor.java index 2822c266b..2e8c95146 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutor.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutor.java @@ -73,13 +73,13 @@ public class PreUpdateProfileActionExecutor { * @param userClaimsToBeDeleted Collection of existing claims that deletes from the profile * @throws UserStoreException If an error occurs while executing the action */ - public void execute(User user, Map userClaimsToBeModified, - Map userClaimsToBeDeleted) throws UserStoreException { + public ActionExecutionStatus execute(User user, Map userClaimsToBeModified, + Map userClaimsToBeDeleted) throws UserStoreException { ActionExecutorService actionExecutorService = SCIMCommonComponentHolder.getActionExecutorService(); if (!actionExecutorService.isExecutionEnabled(ActionType.PRE_UPDATE_PROFILE)) { - return; + return null; } LOG.debug("Executing pre update profile action for user: " + user.getId()); @@ -96,7 +96,7 @@ public void execute(User user, Map userClaimsToBeModified, actionExecutorService.execute(ActionType.PRE_UPDATE_PROFILE, flowContext, IdentityContext.getThreadLocalIdentityContext().getTenantDomain()); - handleActionExecutionStatus(actionExecutionStatus); + return handleActionExecutionStatus(actionExecutionStatus); } catch (ActionExecutionException e) { throw new UserActionExecutionServerException(UserActionError.PRE_UPDATE_PROFILE_ACTION_SERVER_ERROR, "Error while executing pre update profile action.", e); @@ -119,12 +119,12 @@ private UserActionRequestDTO buildUserActionRequestDTO(User user, Map actionExecutionStatus) + private ActionExecutionStatus handleActionExecutionStatus(ActionExecutionStatus actionExecutionStatus) throws UserStoreException { switch (actionExecutionStatus.getStatus()) { case SUCCESS: - return; + return actionExecutionStatus; case FAILED: Failure failure = (Failure) actionExecutionStatus.getResponse(); throw new UserActionExecutionClientException(UserActionError.PRE_UPDATE_PROFILE_ACTION_EXECUTION_FAILED, From 6a8f7c3bd84ff7430bcfccd4568d350443cbfaa8 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Fri, 22 May 2026 10:31:35 +0530 Subject: [PATCH 2/5] test: update SCIMUserManagerTest to mock execution status of PreUpdateProfileActionExecutor --- .../identity/scim2/common/impl/SCIMUserManagerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java index b480bf1b9..ac5149c77 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java @@ -2614,7 +2614,7 @@ public void testUpdateUserClaimsWithNoChanges() throws Exception { PreUpdateProfileActionExecutor executorMock = mock(PreUpdateProfileActionExecutor.class); preUpdateField.set(scimUserManager, executorMock); - doNothing().when(executorMock).execute(any(User.class), anyMap(), anyMap()); + when(executorMock.execute(any(User.class), anyMap(), anyMap())).thenReturn(null); Field carbonUMField = SCIMUserManager.class.getDeclaredField("carbonUM"); carbonUMField.setAccessible(true); @@ -2662,7 +2662,7 @@ public void testUpdateUserClaims_AddDeleteModifyClaims() throws Exception { PreUpdateProfileActionExecutor executorMock = mock(PreUpdateProfileActionExecutor.class); preUpdateField.set(scimUserManager, executorMock); - doNothing().when(executorMock).execute(any(User.class), anyMap(), anyMap()); + when(executorMock.execute(any(User.class), anyMap(), anyMap())).thenReturn(null); Field carbonUMField = SCIMUserManager.class.getDeclaredField("carbonUM"); carbonUMField.setAccessible(true); @@ -2718,7 +2718,7 @@ public void testUpdateUserClaims_MultiValuedClaims() throws Exception { PreUpdateProfileActionExecutor executorMock = mock(PreUpdateProfileActionExecutor.class); preUpdateField.set(scimUserManager, executorMock); - doNothing().when(executorMock).execute(any(User.class), anyMap(), anyMap()); + when(executorMock.execute(any(User.class), anyMap(), anyMap())).thenReturn(null); Field carbonUMField = SCIMUserManager.class.getDeclaredField("carbonUM"); carbonUMField.setAccessible(true); From afb1e73cfdcb651164ab5f0772e6170732e7cc57 Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Tue, 26 May 2026 18:43:54 +0530 Subject: [PATCH 3/5] refactor: pre-update profile action to return execution status and update tests --- .../identity/scim2/common/impl/SCIMUserManager.java | 2 +- .../action/PreUpdateProfileActionExecutor.java | 12 ++++++++++-- .../scim2/common/impl/SCIMUserManagerTest.java | 6 +++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java index 89080ef30..14b1c8c65 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManager.java @@ -5935,7 +5935,7 @@ private void updateUserClaims(User user, Map oldClaimList, populateMultiValuedClaimsToModify(oldClaimList, simpleMultiValuedClaimsToBeAdded, simpleMultiValuedClaimsToBeRemoved, userClaimsToBeModifiedIncludingMultiValueClaims); - ActionExecutionStatus actionExecutionStatus = preUpdateProfileActionExecutor.execute(user, + ActionExecutionStatus actionExecutionStatus = preUpdateProfileActionExecutor.executeAndGetStatus(user, userClaimsToBeModifiedIncludingMultiValueClaims, userClaimsToBeDeleted); if (actionExecutionStatus != null) { diff --git a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutor.java b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutor.java index 2e8c95146..963bd6231 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutor.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/main/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutor.java @@ -73,8 +73,16 @@ public class PreUpdateProfileActionExecutor { * @param userClaimsToBeDeleted Collection of existing claims that deletes from the profile * @throws UserStoreException If an error occurs while executing the action */ - public ActionExecutionStatus execute(User user, Map userClaimsToBeModified, - Map userClaimsToBeDeleted) throws UserStoreException { + public void execute(User user, Map userClaimsToBeModified, + Map userClaimsToBeDeleted) throws UserStoreException { + + executeAndGetStatus(user, userClaimsToBeModified, userClaimsToBeDeleted); + } + + // Triggers the execution of pre update profile extension at profile update with PUT and returns execution status. + public ActionExecutionStatus executeAndGetStatus(User user, Map userClaimsToBeModified, + Map userClaimsToBeDeleted) + throws UserStoreException { ActionExecutorService actionExecutorService = SCIMCommonComponentHolder.getActionExecutorService(); diff --git a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java index 89a232aa4..d17087e9c 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java @@ -2620,7 +2620,7 @@ public void testUpdateUserClaimsWithNoChanges() throws Exception { PreUpdateProfileActionExecutor executorMock = mock(PreUpdateProfileActionExecutor.class); preUpdateField.set(scimUserManager, executorMock); - when(executorMock.execute(any(User.class), anyMap(), anyMap())).thenReturn(null); + doNothing().when(executorMock).execute(any(User.class), anyMap(), anyMap()); Field carbonUMField = SCIMUserManager.class.getDeclaredField("carbonUM"); carbonUMField.setAccessible(true); @@ -2668,7 +2668,7 @@ public void testUpdateUserClaims_AddDeleteModifyClaims() throws Exception { PreUpdateProfileActionExecutor executorMock = mock(PreUpdateProfileActionExecutor.class); preUpdateField.set(scimUserManager, executorMock); - when(executorMock.execute(any(User.class), anyMap(), anyMap())).thenReturn(null); + doNothing().when(executorMock).execute(any(User.class), anyMap(), anyMap()); Field carbonUMField = SCIMUserManager.class.getDeclaredField("carbonUM"); carbonUMField.setAccessible(true); @@ -2724,7 +2724,7 @@ public void testUpdateUserClaims_MultiValuedClaims() throws Exception { PreUpdateProfileActionExecutor executorMock = mock(PreUpdateProfileActionExecutor.class); preUpdateField.set(scimUserManager, executorMock); - when(executorMock.execute(any(User.class), anyMap(), anyMap())).thenReturn(null); + doNothing().when(executorMock).execute(any(User.class), anyMap(), anyMap()); Field carbonUMField = SCIMUserManager.class.getDeclaredField("carbonUM"); carbonUMField.setAccessible(true); From c7a97f6030a9da9b306d90d76d80b07bc6259c3a Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Wed, 27 May 2026 18:23:49 +0530 Subject: [PATCH 4/5] test: add unit tests for executeAndGetStatus in PreUpdateProfileActionExecutor --- .../PreUpdateProfileActionExecutorTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutorTest.java b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutorTest.java index c003f1add..e41d58b24 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutorTest.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/internal/action/PreUpdateProfileActionExecutorTest.java @@ -163,6 +163,51 @@ public void testExecutionWhenActionExecutionIsDisabled() throws Exception { Collections.emptyMap(), Collections.emptyMap())); } + @Test + public void testExecuteAndGetStatusReturnsNullWhenActionExecutionIsDisabled() throws Exception { + + when(actionExecutorService.isExecutionEnabled(ActionType.PRE_UPDATE_PROFILE)).thenReturn(false); + + User user = getSCIMUser(); + ActionExecutionStatus status = + preUpdateProfileActionExecutor.executeAndGetStatus(user, Collections.emptyMap(), + Collections.emptyMap()); + + assertEquals(status, null); + verify(actionExecutorService, Mockito.never()).execute(any(), any(), any()); + } + + @Test + public void testExecuteAndGetStatusReturnsSuccessStatusWhenActionSucceeds() throws Exception { + + setOrganizationToIdentityContext(); + when(actionExecutorService.isExecutionEnabled(ActionType.PRE_UPDATE_PROFILE)).thenReturn(true); + + ActionExecutionStatus successStatus = mock(ActionExecutionStatus.class); + when(successStatus.getStatus()).thenReturn(ActionExecutionStatus.Status.SUCCESS); + when(actionExecutorService.execute(eq(ActionType.PRE_UPDATE_PROFILE), any(), any())).thenReturn(successStatus); + + LocalClaim newClaim = getMockedLocalClaim(NEW_SINGLEVALUE_CLAIM1); + LocalClaim deletingClaim = getMockedLocalClaim(DELETING_SINGLEVALUE_CLAIM4); + when(claimMetadataManagementService.getLocalClaim(eq(NEW_SINGLEVALUE_CLAIM1.getClaimURI()), any(String.class))) + .thenReturn(Optional.of(newClaim)); + when(claimMetadataManagementService.getLocalClaim(eq(DELETING_SINGLEVALUE_CLAIM4.getClaimURI()), + any(String.class))).thenReturn(Optional.of(deletingClaim)); + + User user = getSCIMUser(); + Map claimsToModify = new HashMap<>(); + claimsToModify.put(NEW_SINGLEVALUE_CLAIM1.getClaimURI(), NEW_SINGLEVALUE_CLAIM1.getInputValueAsString()); + Map claimsToDelete = new HashMap<>(); + claimsToDelete.put(DELETING_SINGLEVALUE_CLAIM4.getClaimURI(), + DELETING_SINGLEVALUE_CLAIM4.getInputValueAsString()); + + ActionExecutionStatus status = + preUpdateProfileActionExecutor.executeAndGetStatus(user, claimsToModify, claimsToDelete); + + assertNotNull(status); + assertEquals(status, successStatus); + } + @Test public void testSuccessExecutionForClaimUpdateExecutionAtPutOperation() throws Exception { From 6589058f590aa8eced8cbac024ed2949c10edd6c Mon Sep 17 00:00:00 2001 From: Lashen1227 Date: Wed, 27 May 2026 23:44:28 +0530 Subject: [PATCH 5/5] test: add unit tests for updating user claims with action response --- .../common/impl/SCIMUserManagerTest.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java index d17087e9c..252a00aec 100644 --- a/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java +++ b/components/org.wso2.carbon.identity.scim2.common/src/test/java/org/wso2/carbon/identity/scim2/common/impl/SCIMUserManagerTest.java @@ -42,6 +42,7 @@ import org.wso2.carbon.identity.compatibility.settings.core.exception.CompatibilitySettingException; import org.wso2.carbon.identity.compatibility.settings.core.model.CompatibilitySetting; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionStatus; import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException; import org.wso2.carbon.identity.application.common.model.InboundProvisioningConfig; import org.wso2.carbon.identity.application.common.model.ServiceProvider; @@ -2753,6 +2754,139 @@ public void testUpdateUserClaims_MultiValuedClaims() throws Exception { } } + @Test + public void testUpdateUserClaims_WhenActionResponseContainsAddedModifiedRemovedClaims() throws Exception { + + SCIMUserManager scimUserManager = spy(new SCIMUserManager( + mockedUserStoreManager, + mockClaimMetadataManagementService, + MultitenantConstants.SUPER_TENANT_DOMAIN_NAME + )); + User user = mock(User.class); + when(user.getId()).thenReturn("foo"); + + String accountLocked = "http://wso2.org/claims/identity/accountLocked"; + String accountState = "http://wso2.org/claims/identity/accountState"; + String accountDisabled = "http://wso2.org/claims/identity/accountDisabled"; + + Map oldClaims = new HashMap<>(); + oldClaims.put(accountLocked, "old"); + oldClaims.put(accountState, "deleteMe"); + + Map newClaims = new HashMap<>(); + newClaims.put(accountLocked, "new"); + + Map multiValuedClaims = Collections.emptyMap(); + + Field preUpdateField = SCIMUserManager.class.getDeclaredField("preUpdateProfileActionExecutor"); + preUpdateField.setAccessible(true); + PreUpdateProfileActionExecutor executorMock = mock(PreUpdateProfileActionExecutor.class); + preUpdateField.set(scimUserManager, executorMock); + + ActionExecutionStatus actionExecutionStatus = mock(ActionExecutionStatus.class); + Map responseContext = new HashMap<>(); + Map addedFromAction = new HashMap<>(); + addedFromAction.put(accountDisabled, "addedValue"); + Map modifiedFromAction = new HashMap<>(); + modifiedFromAction.put(accountState, "modifiedValue"); + Map removedFromAction = new HashMap<>(); + removedFromAction.put(accountLocked, ""); + + responseContext.put("userClaimsToBeAdded", addedFromAction); + responseContext.put("userClaimsToBeModified", modifiedFromAction); + responseContext.put("userClaimsToBeRemoved", removedFromAction); + when(actionExecutionStatus.getResponseContext()).thenReturn(responseContext); + scimCommonUtils.when(() -> SCIMCommonUtils.isExecutableUserProfileUpdate(anyMap(), anyMap(), anyMap(), anyMap())) + .thenReturn(true); + doReturn(actionExecutionStatus).when(executorMock) + .executeAndGetStatus(any(User.class), anyMap(), anyMap()); + + Field carbonUMField = SCIMUserManager.class.getDeclaredField("carbonUM"); + carbonUMField.setAccessible(true); + carbonUMField.set(scimUserManager, mockedUserStoreManager); + + doNothing().when(mockedUserStoreManager).setUserClaimValuesWithID(anyString(), anyMap(), isNull()); + doNothing().when(mockedUserStoreManager).deleteUserClaimValueWithID(anyString(), anyString(), isNull()); + + Method m = SCIMUserManager.class.getDeclaredMethod( + "updateUserClaims", User.class, Map.class, Map.class, Map.class + ); + m.setAccessible(true); + m.invoke(scimUserManager, user, oldClaims, newClaims, multiValuedClaims); + + verify(mockedUserStoreManager).setUserClaimValuesWithID(eq("foo"), + argThat(map -> map.containsKey(accountDisabled) + && map.containsKey(accountState) + && !map.containsKey(accountLocked)), + isNull()); + } + + @Test + public void testUpdateUserClaims_WhenActionResponseContainsMultiValuedClaims() throws Exception { + + SCIMUserManager scimUserManager = spy(new SCIMUserManager( + mockedUserStoreManager, + mockClaimMetadataManagementService, + MultitenantConstants.SUPER_TENANT_DOMAIN_NAME + )); + User user = mock(User.class); + when(user.getId()).thenReturn("foo"); + + Map oldClaims = new HashMap<>(); + oldClaims.put("emails", "a@wso2.com,b@wso2.com"); + + Map newClaims = new HashMap<>(); + newClaims.put("emails", "a@wso2.com,b@wso2.com"); + + Map multiValuedClaims = new HashMap<>(); + multiValuedClaims.put("emails", ""); + + Field preUpdateField = SCIMUserManager.class.getDeclaredField("preUpdateProfileActionExecutor"); + preUpdateField.setAccessible(true); + PreUpdateProfileActionExecutor executorMock = mock(PreUpdateProfileActionExecutor.class); + preUpdateField.set(scimUserManager, executorMock); + + ActionExecutionStatus actionExecutionStatus = mock(ActionExecutionStatus.class); + Map responseContext = new HashMap<>(); + Map> multiAddedFromAction = new HashMap<>(); + multiAddedFromAction.put("emails", Collections.singletonList("c@wso2.com")); + Map> multiRemovedFromAction = new HashMap<>(); + multiRemovedFromAction.put("emails", Collections.singletonList("a@wso2.com")); + + responseContext.put("multiValuedClaimsToBeAdded", multiAddedFromAction); + responseContext.put("multiValuedClaimsToBeRemoved", multiRemovedFromAction); + when(actionExecutionStatus.getResponseContext()).thenReturn(responseContext); + scimCommonUtils.when(() -> SCIMCommonUtils.isExecutableUserProfileUpdate(anyMap(), anyMap(), anyMap(), anyMap())) + .thenReturn(true); + doReturn(actionExecutionStatus).when(executorMock) + .executeAndGetStatus(any(User.class), anyMap(), anyMap()); + + Field carbonUMField = SCIMUserManager.class.getDeclaredField("carbonUM"); + carbonUMField.setAccessible(true); + carbonUMField.set(scimUserManager, mockedUserStoreManager); + + try (MockedStatic fw = mockStatic(FrameworkUtils.class)) { + fw.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); + + doNothing().when(mockedUserStoreManager).setUserClaimValuesWithID(anyString(), anyMap(), anyMap(), + anyMap(), anyMap(), isNull()); + + Method m = SCIMUserManager.class.getDeclaredMethod( + "updateUserClaims", User.class, Map.class, Map.class, Map.class + ); + m.setAccessible(true); + m.invoke(scimUserManager, user, oldClaims, newClaims, multiValuedClaims); + + verify(mockedUserStoreManager, times(1)).setUserClaimValuesWithID( + eq("foo"), + anyMap(), + argThat(map -> map.containsKey("emails") && map.get("emails").contains("c@wso2.com")), + argThat(map -> map.containsKey("emails") && map.get("emails").contains("a@wso2.com")), + anyMap(), + isNull()); + } + } + @DataProvider(name = "syncedUserAttributesData") public Object[][] syncedUserAttributesData() {