From 6da2a68c9c50f702d2aa7f8419a5910736f93f90 Mon Sep 17 00:00:00 2001 From: kumuditha Date: Fri, 13 Sep 2024 16:58:21 +0530 Subject: [PATCH 01/14] Update username recovery support for shared claims --- .../impl/UserAccountRecoveryManager.java | 90 ++++++++++++++++--- .../username/UsernameRecoveryManagerImpl.java | 63 ++++++------- 2 files changed, 113 insertions(+), 40 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index 5f235afbf6..1a543b7ea4 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.org) * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -18,7 +18,6 @@ package org.wso2.carbon.identity.recovery.internal.service.impl; import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -69,7 +68,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -360,6 +358,84 @@ public String getUsernameByClaims(Map claims, String tenantDomai } } + /** + * Get the user list for the given claims. + * + * @param claims List of UserClaims + * @param tenantDomain Tenant domain + * @return Username (Return null if there are no users). + * @throws IdentityRecoveryException Error while retrieving the users list. + */ + public ArrayList getUserListByClaims(Map claims, String tenantDomain) + throws IdentityRecoveryException { + + ArrayList resultedUserList = new ArrayList<>(); + + if (MapUtils.isEmpty(claims)) { + // Get error code with scenario. + String errorCode = Utils.prependOperationScenarioToErrorCode( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_FIELD_FOUND_FOR_USER_RECOVERY.getCode(), + IdentityRecoveryConstants.USER_ACCOUNT_RECOVERY); + throw Utils.handleClientException(errorCode, + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_FIELD_FOUND_FOR_USER_RECOVERY.getMessage(), + null); + } + + MultiAttributeLoginService multiAttributeLoginService = IdentityRecoveryServiceDataHolder.getInstance() + .getMultiAttributeLoginService(); + + if (multiAttributeLoginService.isEnabled(tenantDomain) && claims.containsKey(MultiAttributeLoginConstants + .MULTI_ATTRIBUTE_USER_IDENTIFIER_CLAIM_URI)) { + /* Multiple claims are not allowed when user identifier claim is enabled since identifier claim cannot be + used in combination with other claims. */ + if (claims.keySet().size() > 1) { + String errorCode = Utils.prependOperationScenarioToErrorCode( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_CLAIMS_WITH_MULTI_ATTRIBUTE_URI + .getCode(), IdentityRecoveryConstants.USER_ACCOUNT_RECOVERY); + throw Utils.handleClientException(errorCode, + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_CLAIMS_WITH_MULTI_ATTRIBUTE_URI + .getMessage(), null); + } + // Resolve the user with the multi attribute login service. + ResolvedUserResult resolvedUserResult = multiAttributeLoginService.resolveUser( + claims.get(MultiAttributeLoginConstants.MULTI_ATTRIBUTE_USER_IDENTIFIER_CLAIM_URI), tenantDomain); + if (resolvedUserResult != null && ResolvedUserResult.UserResolvedStatus.SUCCESS + .equals(resolvedUserResult.getResolvedStatus())) { + resultedUserList.add(resolvedUserResult.getUser()); + return resultedUserList; + } + return resultedUserList; + } + + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + try { + AbstractUserStoreManager abstractUserStoreManager = (AbstractUserStoreManager) + getUserStoreManager(tenantId); + String userstoreDomain = extractDomainFromClaims(claims, abstractUserStoreManager); + if (userstoreDomain != null) { + populateUserListFromClaimsForDomain(tenantId, claims, userstoreDomain, resultedUserList, + abstractUserStoreManager); + } else { + // If a userstore domain is not specified in the request, consider all userstores. + List userStoreDomainNames = getDomainNames(tenantId); + for (String domain : userStoreDomainNames) { + populateUserListFromClaimsForDomain(tenantId, claims, domain, resultedUserList, + abstractUserStoreManager); + } + } + // When the code reaches here there only be single user match. + return resultedUserList; + } catch (org.wso2.carbon.user.core.UserStoreException e) { + if (log.isDebugEnabled()) { + log.debug("Error while retrieving users from user store for the given claim set: " + + Arrays.toString(claims.keySet().toArray())); + } + throw new IdentityRecoveryException(e.getErrorCode(), "Error occurred while retrieving users.", e); + } catch (UserStoreException | IdentityRecoveryServerException e) { + throw new IdentityRecoveryException(e.getMessage(), e); + } + } + /** * Extract and remove the userstore domain from the claim set. * @@ -423,13 +499,7 @@ private void populateUserListFromClaimsForDomain(int tenantId, Map 1) { - log.warn("Multiple users matched for given claims set: " + claims.keySet()); - throw Utils.handleClientException( - IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS, null); - } + UserCoreConstants.DEFAULT_PROFILE, Integer.MAX_VALUE, 1, null, null)); } } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java index 5fd5ebcf9a..2622d490bc 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java @@ -53,6 +53,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -90,40 +91,42 @@ public RecoveryInformationDTO initiate(Map claims, String tenant boolean manageNotificationsInternally = Utils.isNotificationsInternallyManaged(tenantDomain, properties); if (useLegacyAPIApproach) { // Use legacy API approach to support legacy username recovery. - String username = userAccountRecoveryManager.getUsernameByClaims(claims, tenantDomain); - if (StringUtils.isNotEmpty(username)) { - if (manageNotificationsInternally) { - User user = createUser(username, tenantDomain); - triggerNotification(user, NotificationChannels.EMAIL_CHANNEL.getChannelType(), - IdentityEventConstants.Event.TRIGGER_NOTIFICATION, null); + ArrayList resultedUserList = userAccountRecoveryManager + .getUserListByClaims(claims, tenantDomain); + for (org.wso2.carbon.user.core.common.User recoveredUser : resultedUserList) { + String username = recoveredUser.getDomainQualifiedUsername(); + if (StringUtils.isNotEmpty(username)) { + if (manageNotificationsInternally) { + User user = createUser(username, tenantDomain); + triggerNotification(user, NotificationChannels.EMAIL_CHANNEL.getChannelType(), + IdentityEventConstants.Event.TRIGGER_NOTIFICATION, null); + if (log.isDebugEnabled()) { + log.debug("Successful username recovery for user: " + username + ". " + + "User notified Internally"); + } + auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, NOTIFICATION_TYPE_INTERNAL, + username, null, FrameworkConstants.AUDIT_SUCCESS); + } if (log.isDebugEnabled()) { - log.debug("Successful username recovery for user: " + username + ". " + - "User notified Internally"); + log.debug("Successful username recovery for user: " + username + ". User notified Externally"); } - auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, NOTIFICATION_TYPE_INTERNAL, + auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, NOTIFICATION_TYPE_EXTERNAL, username, null, FrameworkConstants.AUDIT_SUCCESS); - return null; - } - if (log.isDebugEnabled()) { - log.debug("Successful username recovery for user: " + username + ". User notified Externally"); - } - auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, NOTIFICATION_TYPE_EXTERNAL, - username, null, FrameworkConstants.AUDIT_SUCCESS); - recoveryInformationDTO.setUsername(username); - } else { - String errorMsg = - String.format("No user found for the given claims in tenant domain : %s", tenantDomain); - if (log.isDebugEnabled()) { - log.debug(errorMsg); - } - auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, "N/A", username, errorMsg, - FrameworkConstants.AUDIT_FAILED); - if (Boolean.parseBoolean( - IdentityUtil.getProperty(IdentityRecoveryConstants.ConnectorConfig.NOTIFY_USER_EXISTENCE))) { - throw Utils.handleClientException( - IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null); + recoveryInformationDTO.setUsername(username); + } else { + String errorMsg = + String.format("No user found for the given claims in tenant domain : %s", tenantDomain); + if (log.isDebugEnabled()) { + log.debug(errorMsg); + } + auditUserNameRecovery(AuditConstants.ACTION_USERNAME_RECOVERY, claims, "N/A", username, errorMsg, + FrameworkConstants.AUDIT_FAILED); + if (Boolean.parseBoolean( + IdentityUtil.getProperty(IdentityRecoveryConstants.ConnectorConfig.NOTIFY_USER_EXISTENCE))) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null); + } } - return null; } return recoveryInformationDTO; } From 26a9e5e3a75b7afc302838fe63ee314a3aa88071 Mon Sep 17 00:00:00 2001 From: kumuditha Date: Fri, 13 Sep 2024 16:58:46 +0530 Subject: [PATCH 02/14] Fix unit tests for username recovery support for shared claims --- .../impl/UserAccountRecoveryManagerTest.java | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java index 1f4274da1c..d700a12e3f 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java @@ -53,6 +53,7 @@ import org.wso2.carbon.user.core.service.RealmService; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.UUID; @@ -62,6 +63,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -426,27 +428,19 @@ private void testNoClaimsProvidedToRetrieveMatchingUsers() { private void testMultipleUsersMatchingForGivenClaims() throws Exception { mockUserstoreManager(); - try { - mockedUtils.when(() -> Utils.handleClientException( - IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS, null)) - .thenReturn(IdentityException.error(IdentityRecoveryClientException.class, - IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS.getCode(),"")); - when(abstractUserStoreManager.getUserListWithID(any(Condition.class),anyString(),anyString(), - anyInt(),anyInt(),isNull(), isNull())).thenReturn(getFilteredUsers()); - when(claimManager.getAttributeName(anyString(),anyString())). - thenReturn("http://wso2.org/claims/mockedClaim"); - when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) - .thenReturn(multiAttributeLoginService); - when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); - String username = userAccountRecoveryManager - .getUsernameByClaims(userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); - assertNull(username, "UserAccountRecoveryManager: Exception should be thrown. Therefore, a " - + "value for an identified user cannot be returned : "); - } catch (IdentityRecoveryException e) { - assertEquals(e.getErrorCode(), - IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS.getCode(), - "Invalid error code for existing multiple users for given set of claims"); - } + org.wso2.carbon.user.core.common.User testUser = mock(org.wso2.carbon.user.core.common.User.class); + List users = Arrays.asList(testUser, testUser, testUser); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(users); + when(claimManager.getAttributeName(anyString(), anyString())).thenReturn("http://wso2.org/claims/mockedClaim"); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()).thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + + IdentityEventService identityEventService = mock(IdentityEventService.class); + when(IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService()).thenReturn(identityEventService); + + ArrayList list = userAccountRecoveryManager.getUserListByClaims(userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + assertEquals(3,list.size()); } From 7141936971ae03c3caaf8003984d3115921b64ce Mon Sep 17 00:00:00 2001 From: kumuditha Date: Fri, 13 Sep 2024 17:07:50 +0530 Subject: [PATCH 03/14] Remove code comments --- .../internal/service/impl/UserAccountRecoveryManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index 1a543b7ea4..8b3ad46b6f 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -423,7 +423,6 @@ public ArrayList getUserListByClaims(Map< abstractUserStoreManager); } } - // When the code reaches here there only be single user match. return resultedUserList; } catch (org.wso2.carbon.user.core.UserStoreException e) { if (log.isDebugEnabled()) { @@ -735,6 +734,7 @@ private String getLocalClaimMaskingRegex(String claimURI, String tenantDomain) return localClaimMaskingRegex; } + //TODO remove /** * Get the users list for a matching claim. * @@ -858,6 +858,7 @@ private UserStoreManager getUserStoreManager(User user) throws IdentityRecoveryE } } + //TODO remove /** * Keep the common users list from the previously matched list and the new list. * From ca3a39b8c11ad09d35d3052c717477b083198f10 Mon Sep 17 00:00:00 2001 From: kumuditha Date: Fri, 13 Sep 2024 17:10:41 +0530 Subject: [PATCH 04/14] Remove unused methods --- .../impl/UserAccountRecoveryManager.java | 78 ------------------- 1 file changed, 78 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index 8b3ad46b6f..e6989793bd 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -734,50 +734,6 @@ private String getLocalClaimMaskingRegex(String claimURI, String tenantDomain) return localClaimMaskingRegex; } - //TODO remove - /** - * Get the users list for a matching claim. - * - * @param tenantId Tenant ID - * @param claimUri Claim to be searched - * @param claimValue Claim value to be matched - * @return Matched users list - * @throws IdentityRecoveryServerException If an error occurred while retrieving claims from the userstore manager. - */ - private String[] getUserList(int tenantId, String claimUri, String claimValue) - throws IdentityRecoveryServerException { - - String[] userList = new String[0]; - UserStoreManager userStoreManager = getUserStoreManager(tenantId); - try { - if (userStoreManager != null) { - if (StringUtils.isNotBlank(claimValue) && claimValue.contains(FORWARD_SLASH)) { - String extractedDomain = IdentityUtil.extractDomainFromName(claimValue); - UserStoreManager secondaryUserStoreManager = userStoreManager. - getSecondaryUserStoreManager(extractedDomain); - /* - Some claims (Eg:- Birth date) can have "/" in claim values. But in user store level we are trying - to extract the claim value and find the user store domain. Hence we are adding an extra "/" to - the claim value to avoid such issues. - */ - if (secondaryUserStoreManager == null) { - claimValue = FORWARD_SLASH + claimValue; - } - } - userList = userStoreManager.getUserList(claimUri, claimValue, null); - } - return userList; - } catch (UserStoreException e) { - if (log.isDebugEnabled()) { - String error = String - .format("Unable to retrieve the claim : %1$s for the given tenant : %2$s", claimUri, tenantId); - log.debug(error, e); - } - throw Utils.handleServerException( - IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_ERROR_RETRIEVING_USER_CLAIM, claimUri, e); - } - } - /** * Get all the domain names related to user stores. * @param tenantId Tenant ID @@ -858,40 +814,6 @@ private UserStoreManager getUserStoreManager(User user) throws IdentityRecoveryE } } - //TODO remove - /** - * Keep the common users list from the previously matched list and the new list. - * - * @param resultedUserList Already matched users for previous claims - * @param matchedUserList Retrieved users list for the given claim - * @param claim Claim used for filtering - * @param value Value given for the claim - * @return Users list with no duplicates. - */ - private String[] getCommonUserEntries(String[] resultedUserList, String[] matchedUserList, String claim, - String value) { - - ArrayList matchedUsers = new ArrayList<>(Arrays.asList(matchedUserList)); - ArrayList resultedUsers = new ArrayList<>(Arrays.asList(resultedUserList)); - // Remove not matching users. - resultedUsers.retainAll(matchedUsers); - if (resultedUsers.size() > 0) { - resultedUserList = resultedUsers.toArray(new String[0]); - if (log.isDebugEnabled()) { - log.debug("Current matching temporary user list :" + Arrays.toString(resultedUserList)); - } - return resultedUserList; - } else { - if (log.isDebugEnabled()) { - String message = String - .format("There are no common users for claim : %1$s with the value : %2$s with the " - + "previously filtered user list", claim, value); - log.debug(message); - } - return new String[0]; - } - } - /** * Get claim values of a user for a given list of claims. * From cd6ffabfa739a70a9d39e8ecbe7b36113346d7c1 Mon Sep 17 00:00:00 2001 From: kumuditha Date: Sun, 15 Sep 2024 14:21:40 +0530 Subject: [PATCH 05/14] Fix javadoc comments --- .../internal/service/impl/UserAccountRecoveryManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index e6989793bd..d5e8460222 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -363,7 +363,7 @@ public String getUsernameByClaims(Map claims, String tenantDomai * * @param claims List of UserClaims * @param tenantDomain Tenant domain - * @return Username (Return null if there are no users). + * @return resultedUserList (Returns an empty list if there are no users). * @throws IdentityRecoveryException Error while retrieving the users list. */ public ArrayList getUserListByClaims(Map claims, String tenantDomain) From 2999a5c5be068b3332f628cb7e6425c7093b79b4 Mon Sep 17 00:00:00 2001 From: kumuditha Date: Wed, 18 Sep 2024 16:02:06 +0530 Subject: [PATCH 06/14] Add new unit tests --- .../impl/UserAccountRecoveryManagerTest.java | 641 +++++++++++++++++- 1 file changed, 623 insertions(+), 18 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java index d700a12e3f..27d45d467b 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,13 @@ package org.wso2.carbon.identity.recovery.internal.service.impl; import org.apache.commons.lang.StringUtils; -import org.mockito.ArgumentMatchers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.identity.application.common.model.User; @@ -32,13 +30,19 @@ import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataManagementService; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.event.IdentityEventConstants; +import org.wso2.carbon.identity.event.IdentityEventException; +import org.wso2.carbon.identity.event.event.Event; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; +import org.wso2.carbon.identity.multi.attribute.login.constants.MultiAttributeLoginConstants; import org.wso2.carbon.identity.multi.attribute.login.mgt.MultiAttributeLoginService; import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; import org.wso2.carbon.identity.recovery.IdentityRecoveryException; +import org.wso2.carbon.identity.recovery.IdentityRecoveryServerException; import org.wso2.carbon.identity.recovery.RecoveryScenarios; +import org.wso2.carbon.identity.recovery.RecoverySteps; import org.wso2.carbon.identity.recovery.dto.NotificationChannelDTO; import org.wso2.carbon.identity.recovery.dto.RecoveryChannelInfoDTO; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; @@ -47,6 +51,7 @@ import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.core.UserStoreException; import org.wso2.carbon.user.core.claim.ClaimManager; import org.wso2.carbon.user.core.common.AbstractUserStoreManager; import org.wso2.carbon.user.core.model.Condition; @@ -63,11 +68,17 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; /** @@ -105,6 +116,9 @@ public class UserAccountRecoveryManagerTest { @Mock MultiAttributeLoginService multiAttributeLoginService; + @Mock + UserRecoveryDataStore mockUserRecoveryDataStore; + /** * User claims map. */ @@ -118,11 +132,11 @@ public class UserAccountRecoveryManagerTest { @BeforeMethod public void setUp() { - mockedJDBCRecoveryDataStore = Mockito.mockStatic(JDBCRecoveryDataStore.class); - mockedIdentityUtil = Mockito.mockStatic(IdentityUtil.class); - mockedUtils = Mockito.mockStatic(Utils.class); - mockedIdentityTenantUtil = Mockito.mockStatic(IdentityTenantUtil.class); - mockedIdentityRecoveryServiceDataHolder = Mockito.mockStatic(IdentityRecoveryServiceDataHolder.class); + mockedJDBCRecoveryDataStore = mockStatic(JDBCRecoveryDataStore.class); + mockedIdentityUtil = mockStatic(IdentityUtil.class); + mockedUtils = mockStatic(Utils.class); + mockedIdentityTenantUtil = mockStatic(IdentityTenantUtil.class); + mockedIdentityRecoveryServiceDataHolder = mockStatic(IdentityRecoveryServiceDataHolder.class); } @AfterMethod @@ -138,7 +152,7 @@ public void tearDown() { @BeforeTest private void setup() { - MockitoAnnotations.openMocks(this); + openMocks(this); userAccountRecoveryManager = UserAccountRecoveryManager.getInstance(); userClaims = buildUserClaimsMap(); } @@ -159,6 +173,595 @@ public void testRetrieveUserRecoveryInformation() throws Exception { testGetUserWithNotificationsInternallyManaged(); } + /** + * Tests that a NullPointerException is thrown during user recovery when a UserStoreException + * occurs. + * + * @throws Exception if mocking or method invocation fails. + */ + @Test + public void testThrowNullPointerForUserRecovery() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(getOneFilteredUser()); + when(claimManager.getAttributeName(anyString(), anyString())) + .thenReturn("http://wso2.org/claims/mockedClaim"); + mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY"); + HashMap properties = new HashMap<>(); + properties.put(IdentityEventConstants.EventProperty.USER, "user"); + when(IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService()) + .thenReturn(identityEventService); + + Throwable cause = new Throwable("error"); + doThrow(new UserStoreException(cause)) + .when(realmService).getTenantUserRealm(anyInt()); + + when(Utils.handleServerException + (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_ERROR_GETTING_USERSTORE_MANAGER, null, cause)) + .thenReturn(new IdentityRecoveryServerException(null, null, null)); + + assertThrows(NullPointerException.class, () -> { + userAccountRecoveryManager.retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, + RecoveryScenarios.USERNAME_RECOVERY, properties); + }); + openMocks(this); + } + + /** + * Tests that an empty user list is returned when retrieving users by claims with an empty claim set. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testEmptyUserListByClaims() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(true); + HashMap emptyUserClaims = new HashMap<>(); + emptyUserClaims.put(MultiAttributeLoginConstants.MULTI_ATTRIBUTE_USER_IDENTIFIER_CLAIM_URI, "testURI"); + ArrayList userlist = + userAccountRecoveryManager + .getUserListByClaims(emptyUserClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + assertEquals(userlist.size(), 0); + } + + /** + * Tests that an IdentityRecoveryClientException is thrown during user recovery when an + * IdentityEventException is triggered and specific mock conditions are met. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testThrowClientExceptionForUserRecovery() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(getOneFilteredUser()); + when(claimManager.getAttributeName(anyString(), anyString())) + .thenReturn("http://wso2.org/claims/mockedClaim"); + mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY"); + HashMap properties = new HashMap<>(); + properties.put(IdentityEventConstants.EventProperty.USER, "user"); + when(IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService()) + .thenReturn(identityEventService); + doThrow(new IdentityEventException("error")) + .when(identityEventService).handleEvent(any(Event.class)); + when(Utils.handleClientException("UNR-10003", + "error", "sominda1")) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + assertThrows(IdentityRecoveryClientException.class, () -> { + userAccountRecoveryManager.retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, + RecoveryScenarios.USERNAME_RECOVERY, properties); + }); + } + + /** + * Tests that an IdentityRecoveryClientException is thrown during user recovery when an + * IdentityEventException occurs and the primary domain name is different from the expected value. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testClientExceptionWithDifferentDomain() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(getOneFilteredUser()); + when(claimManager.getAttributeName(anyString(), anyString())) + .thenReturn("http://wso2.org/claims/mockedClaim"); + mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY1"); + HashMap properties = new HashMap<>(); + properties.put(IdentityEventConstants.EventProperty.USER, "user"); + when(IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService()) + .thenReturn(identityEventService); + doThrow(new IdentityEventException("error")) + .when(identityEventService).handleEvent(any(Event.class)); + when(Utils.handleClientException("UNR-10003", + "error", "sominda1")) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + assertThrows(IdentityRecoveryClientException.class, () -> { + userAccountRecoveryManager.retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, + RecoveryScenarios.USERNAME_RECOVERY, properties); + }); + } + + /** + * Tests that an IdentityRecoveryException is thrown during user recovery when the account is locked. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testRetrieveUserRecoveryThrowsForLockedAccount() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(getOneFilteredUser()); + when(claimManager.getAttributeName(anyString(), anyString())) + .thenReturn("http://wso2.org/claims/mockedClaim"); + when(Utils.isAccountDisabled(any(User.class))).thenReturn(false); + when(Utils.isAccountLocked(any(User.class))).thenReturn(true); + when(Utils.getAccountState(any(User.class))).thenReturn(null); + String errorCode = Utils.prependOperationScenarioToErrorCode( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_LOCKED_ACCOUNT.getCode(), + IdentityRecoveryConstants.USER_ACCOUNT_RECOVERY); + when(Utils.handleClientException(errorCode, + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_LOCKED_ACCOUNT.getMessage(), "sominda1")) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, + RecoveryScenarios.USERNAME_RECOVERY, null); + }); + } + + /** + * Tests that an IdentityRecoveryException is thrown when an invalid recovery flow ID is provided. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testThrowInvalidRecoveryFlowIdExceptionWhenInvalidFlowIdIsProvided() throws Exception { + + mockUserRecoveryDataStore = mock(UserRecoveryDataStore.class); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + UserRecoveryData recoveryData = new UserRecoveryData(null, null, null, null); + when(mockUserRecoveryDataStore + .loadRecoveryFlowData(recoveryData)) + .thenThrow(new IdentityRecoveryException + (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_FLOW_ID.getCode(), "testMessage")); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.loadUserRecoveryFlowData(recoveryData); + }); + } + + /** + * Tests that an IdentityRecoveryException is thrown when an expired recovery flow ID is provided. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testThrowExpiredRecoveryFlowIdExceptionWhenExpiredFlowIdIsProvided() throws Exception { + + mockUserRecoveryDataStore = mock(UserRecoveryDataStore.class); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + UserRecoveryData recoveryData = new UserRecoveryData(null, null, null, null); + when(mockUserRecoveryDataStore + .loadRecoveryFlowData(recoveryData)) + .thenThrow(new IdentityRecoveryException + (IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_FLOW_ID.getCode(), "testMessage")); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.loadUserRecoveryFlowData(recoveryData); + }); + } + + /** + * Tests that an IdentityRecoveryException is thrown when an invalid recovery code is provided. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testThrowIdentityRecoveryExceptionForInvalidCode() throws Exception { + + mockUserRecoveryDataStore = mock(UserRecoveryDataStore.class); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + UserRecoveryData recoveryData = new UserRecoveryData(null, null, null, null); + when(mockUserRecoveryDataStore + .loadRecoveryFlowData(recoveryData)) + .thenThrow(new IdentityRecoveryException + ("invalidCode", "error")); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.loadUserRecoveryFlowData(recoveryData); + }); + } + + /** + * Tests that an IdentityRecoveryException is thrown when no recovery flow data is found for the provided flow ID. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testThrowNoRecoveryFlowDataExceptionWhenNoDataIsFound() throws Exception { + + String testFlowId = "testFlowId"; + mockUserRecoveryDataStore = mock(UserRecoveryDataStore.class); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + UserRecoveryData recoveryData = new UserRecoveryData(null, null, null, null); + recoveryData.setRecoveryFlowId(testFlowId); + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_RECOVERY_FLOW_DATA, + testFlowId)) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.loadUserRecoveryFlowData(recoveryData); + }); + } + + /** + * Tests that an IdentityRecoveryException is thrown when a UserStoreException occurs + * while retrieving usernames or user lists by claims. + * + * @param methodName the name of the method to test ('getUsernameByClaims' or 'getUserListByClaims'). + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test(dataProvider = "userStoreErrorDataProvider") + public void testGetUsernameOrUserListByClaimsThrowsExceptionOnUserStoreError(String methodName) throws Exception { + + mockUserstoreManager(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(true); + when(realmService.getTenantUserRealm(anyInt()).getClaimManager()).thenThrow(new UserStoreException()); + if (methodName.equals("getUsernameByClaims")) { + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.getUsernameByClaims(userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + }); + } else if (methodName.equals("getUserListByClaims")) { + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.getUserListByClaims(userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + }); + } + openMocks(this); + } + + @DataProvider(name = "userStoreErrorDataProvider") + public Object[][] provideUserStoreErrorData() { + + return new Object[][]{ + {"getUsernameByClaims"}, + {"getUserListByClaims"} + }; + } + + /** + * Tests that an IdentityRecoveryException is thrown with the specified error code and message + * when loading user recovery data fails. + * + * @param errorCode the error code to be thrown by the exception. + * @param errorMessage the error message to be thrown by the exception. + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test(dataProvider = "identityRecoveryExceptionDataProvider") + public void testThrowsIdentityRecoveryException(String errorCode, String errorMessage) throws Exception { + + mockUserRecoveryDataStore = mock(UserRecoveryDataStore.class); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + when(mockUserRecoveryDataStore.load(anyString())) + .thenThrow(new IdentityRecoveryException(errorCode, errorMessage)); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.getUserRecoveryData("testFlowId", RecoverySteps.SEND_RECOVERY_INFORMATION); + }); + } + + @DataProvider(name = "identityRecoveryExceptionDataProvider") + public Object[][] provideExceptionData() { + + String message = "error"; + return new Object[][]{ + {IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CODE.getCode(), message}, + {IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE.getCode(), message}, + {"invalidCode", message} + }; + } + + /** + * Tests that an IdentityRecoveryException is thrown when no account recovery data is found + * for the provided code. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testNoAccountRecoveryDataThrowsIdentityRecoveryException() throws Exception { + + String code = "UAR-10008"; + mockUserRecoveryDataStore = mock(UserRecoveryDataStore.class); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_ACCOUNT_RECOVERY_DATA, + code)) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.getUserRecoveryData(code, RecoverySteps.SEND_RECOVERY_INFORMATION); + }); + } + + /** + * Tests that an IdentityRecoveryException is thrown when an invalid recovery step is provided. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testInvalidRecoveryStepThrowsIdentityRecoveryException() throws Exception { + + String code = "UAR-10001"; + mockUserRecoveryDataStore = mock(UserRecoveryDataStore.class); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + UserRecoveryData recoveryData = new UserRecoveryData(null, null, null); + recoveryData.setRecoveryFlowId("testFlowId"); + when(mockUserRecoveryDataStore.load(anyString())).thenReturn(recoveryData); + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_RECOVERY_CODE, + code)) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.getUserRecoveryData(code, RecoverySteps.SEND_RECOVERY_INFORMATION); + }); + } + + /** + * Tests the various update operations on user recovery data, including updating failed attempts, + * updating resend count, and invalidating recovery data. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testUpdateRecoveryData() throws Exception { + + mockUserRecoveryDataStore = mock(UserRecoveryDataStore.class); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + userAccountRecoveryManager = UserAccountRecoveryManager.getInstance(); + String recoveryFlowId = "testRecoveryFlowId"; + testUpdateRecoveryDataFailedAttempts(recoveryFlowId); + testUpdateRecoveryDataResendCount(recoveryFlowId); + testInvalidateRecoveryData(recoveryFlowId); + } + + /** + * Tests updating the number of failed attempts in user recovery data. + * + * @param recoveryFlowId the recovery flow ID associated with the data. + * @throws IdentityRecoveryException if there is an issue with updating failed attempts. + */ + public void testUpdateRecoveryDataFailedAttempts(String recoveryFlowId) throws IdentityRecoveryException { + + int failedAttempts = 3; + userAccountRecoveryManager.updateRecoveryDataFailedAttempts(recoveryFlowId, failedAttempts); + verify(mockUserRecoveryDataStore, times(1)).updateFailedAttempts(recoveryFlowId, failedAttempts); + } + + /** + * Tests updating the resend count in user recovery data. + * + * @param recoveryFlowId the recovery flow ID associated with the data. + * @throws IdentityRecoveryException if there is an issue with updating the resend count. + */ + public void testUpdateRecoveryDataResendCount(String recoveryFlowId) throws IdentityRecoveryException { + + int resendCount = 2; + userAccountRecoveryManager.updateRecoveryDataResendCount(recoveryFlowId, resendCount); + verify(mockUserRecoveryDataStore, times(1)).updateCodeResendCount(recoveryFlowId, resendCount); + } + + /** + * Tests invalidating user recovery data based on the recovery flow ID. + * + * @param recoveryFlowId the recovery flow ID associated with the data. + * @throws IdentityRecoveryException if there is an issue with invalidating recovery data. + */ + public void testInvalidateRecoveryData(String recoveryFlowId) throws IdentityRecoveryException { + + userAccountRecoveryManager.invalidateRecoveryData(recoveryFlowId); + verify(mockUserRecoveryDataStore, times(1)).invalidateWithRecoveryFlowId(recoveryFlowId); + } + + /** + * Tests that an IdentityRecoveryException is thrown when retrieving user recovery information + * if the account is locked. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testRetrieveUserRecoveryInformationThrowsExceptionWhenAccountIsLocked() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(getOneFilteredUser()); + when(claimManager.getAttributeName(anyString(), anyString())) + .thenReturn("http://wso2.org/claims/mockedClaim"); + when(Utils.isAccountDisabled(any(User.class))).thenReturn(false); + when(Utils.isAccountLocked(any(User.class))).thenReturn(true); + when(Utils.getAccountState(any(User.class))).thenReturn(IdentityRecoveryConstants.PENDING_SELF_REGISTRATION); + when(Utils.prependOperationScenarioToErrorCode(anyString(), anyString())).thenReturn("UAR-6100"); + when(Utils.handleClientException(anyString(), anyString(), anyString())) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, + RecoveryScenarios.USERNAME_RECOVERY, null); + }); + } + + /** + * Tests that an IdentityRecoveryException is thrown when retrieving user recovery information + * if the account is disabled. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testAccountDisabled() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(getOneFilteredUser()); + when(claimManager.getAttributeName(anyString(), anyString())) + .thenReturn("http://wso2.org/claims/mockedClaim"); + when(Utils.isAccountDisabled(any(User.class))).thenReturn(false); + when(Utils.isAccountLocked(any(User.class))).thenReturn(true); + when(Utils.getAccountState(any(User.class))).thenReturn(IdentityRecoveryConstants.PENDING_ASK_PASSWORD); + when(Utils.prependOperationScenarioToErrorCode(anyString(), anyString())).thenReturn("UAR-17006"); + when(Utils.handleClientException(anyString(), anyString(), anyString())) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, + RecoveryScenarios.USERNAME_RECOVERY, null); + }); + } + + /** + * Tests that an IdentityRecoveryException is thrown when retrieving user recovery information + * if the account is disabled. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testCheckAccountLockedStatus() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(getOneFilteredUser()); + when(claimManager.getAttributeName(anyString(), anyString())) + .thenReturn("http://wso2.org/claims/mockedClaim"); + when(Utils.isAccountDisabled(any(User.class))).thenReturn(true); + when(Utils.prependOperationScenarioToErrorCode(anyString(), anyString())).thenReturn("UAR-17006"); + when(Utils.handleClientException(anyString(), anyString(), anyString())) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, + RecoveryScenarios.USERNAME_RECOVERY, null); + }); + } + + /** + * Tests that the method {@link UserAccountRecoveryManager#getUsernameByClaims} returns an empty + * string when multi-attribute login is enabled and no matching username is found for the provided claims. + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test + public void testGetUsernameByClaimsReturnsEmptyWhenMultiAttributeLoginIsEnabled() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(true); + HashMap userClaims = new HashMap<>(); + userClaims.put(MultiAttributeLoginConstants.MULTI_ATTRIBUTE_USER_IDENTIFIER_CLAIM_URI, "testURI"); + String Username = userAccountRecoveryManager + .getUsernameByClaims(userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + assertEquals(StringUtils.EMPTY, Username); + } + + /** + * Tests that an IdentityRecoveryException is thrown for methods that retrieve user information + * by claims when exceptions are simulated. + * + * @param methodName the name of the method to test. + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test(dataProvider = "getUserByClaimsExceptions") + public void testMethodsThrowException(String methodName) throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(true); + userClaims.put(MultiAttributeLoginConstants.MULTI_ATTRIBUTE_USER_IDENTIFIER_CLAIM_URI, "testURI"); + + when(Utils.prependOperationScenarioToErrorCode(anyString(), anyString())).thenReturn("UAR-20066"); + when(Utils.handleClientException(anyString(), anyString(), isNull())) + .thenReturn(new IdentityRecoveryClientException(null, null, null)); + + if ("getUsernameByClaims".equals(methodName)) { + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.getUsernameByClaims(userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + }); + } else if ("getUserListByClaims".equals(methodName)) { + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.getUserListByClaims(userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + }); + } + } + + @DataProvider(name = "getUserByClaimsExceptions") + public Object[][] exceptionScenarios() { + + return new Object[][] { + { "getUsernameByClaims" }, + { "getUserListByClaims" } + }; + } + + /** + * Tests that an IdentityRecoveryException is thrown when loading user recovery data + * from a recovery flow ID and different error codes are simulated. + * + * @param errorCode the error code to simulate during the test. + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test(dataProvider = "recoveryExceptions") + public void testLoadFromRecoveryFlowIdThrowsException(String errorCode) throws Exception { + + String recoveryFlowId = "testFlowId"; + mockUserRecoveryDataStore = mock(UserRecoveryDataStore.class); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + userAccountRecoveryManager = UserAccountRecoveryManager.getInstance(); + when(Utils.prependOperationScenarioToErrorCode(anyString(), anyString())).thenReturn("UAR-INVALID"); + when(mockUserRecoveryDataStore.loadFromRecoveryFlowId(recoveryFlowId, RecoverySteps.UPDATE_PASSWORD)) + .thenThrow(new IdentityRecoveryException(errorCode, "error")); + assertThrows(IdentityRecoveryException.class, () -> { + userAccountRecoveryManager.getUserRecoveryDataFromFlowId(recoveryFlowId, RecoverySteps.UPDATE_PASSWORD); + }); + } + + @DataProvider(name = "recoveryExceptions") + public Object[][] createRecoveryExceptionsData() { + + return new Object[][] { + { IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_FLOW_ID.getCode() }, + { IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_FLOW_ID.getCode() }, + { IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE.getCode() }, + { "InvalidID" } + }; + } + /** * Test get user recovery when the notifications are internally managed. * @@ -190,7 +793,7 @@ private void testGetGeneralUsers() throws Exception { userClaims.remove(UserProfile.EMAIL_VERIFIED.key); userClaims.remove(UserProfile.PHONE_VERIFIED.key); when(abstractUserStoreManager - .getUserClaimValues(anyString(), ArgumentMatchers.any(String[].class), anyString())) + .getUserClaimValues(anyString(), any(String[].class), anyString())) .thenReturn(userClaims); when(abstractUserStoreManager.getUserListWithID(any(Condition.class),anyString(),anyString(), anyInt(),anyInt(),isNull(), isNull())).thenReturn(getOneFilteredUser()); @@ -218,7 +821,7 @@ private void testGetSelfSignUpUsers() throws Exception { when(abstractUserStoreManager.getUserListWithID(any(Condition.class),anyString(),anyString(), anyInt(),anyInt(),isNull(), isNull())).thenReturn(getOneFilteredUser()); when(abstractUserStoreManager - .getUserClaimValues(anyString(), ArgumentMatchers.any(String[].class), isNull())) + .getUserClaimValues(anyString(), any(String[].class), isNull())) .thenReturn(userClaims); RecoveryChannelInfoDTO recoveryChannelInfoDTO = userAccountRecoveryManager .retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, RecoveryScenarios.USERNAME_RECOVERY, @@ -318,8 +921,8 @@ private void testNoMatchingUsersForGivenClaims() throws Exception { private void mockJDBCRecoveryDataStore() throws IdentityRecoveryException { mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(userRecoveryDataStore); - doNothing().when(userRecoveryDataStore).invalidate(ArgumentMatchers.any(User.class)); - doNothing().when(userRecoveryDataStore).store(ArgumentMatchers.any(UserRecoveryData.class)); + doNothing().when(userRecoveryDataStore).invalidate(any(User.class)); + doNothing().when(userRecoveryDataStore).store(any(UserRecoveryData.class)); } /** @@ -332,8 +935,8 @@ private void mockRecoveryConfigs(boolean isNotificationInternallyManaged) throws mockedIdentityUtil.when(() -> IdentityUtil.extractDomainFromName(anyString())).thenReturn("PRIMARY"); mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY"); - mockedUtils.when(() -> Utils.isAccountDisabled(ArgumentMatchers.any(User.class))).thenReturn(false); - mockedUtils.when(() -> Utils.isAccountLocked(ArgumentMatchers.any(User.class))).thenReturn(false); + mockedUtils.when(() -> Utils.isAccountDisabled(any(User.class))).thenReturn(false); + mockedUtils.when(() -> Utils.isAccountLocked(any(User.class))).thenReturn(false); mockedUtils.when(() -> Utils.isNotificationsInternallyManaged(anyString(), isNull())) .thenReturn(isNotificationInternallyManaged); } @@ -439,7 +1042,9 @@ private void testMultipleUsersMatchingForGivenClaims() throws Exception { IdentityEventService identityEventService = mock(IdentityEventService.class); when(IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService()).thenReturn(identityEventService); - ArrayList list = userAccountRecoveryManager.getUserListByClaims(userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + ArrayList list = + userAccountRecoveryManager.getUserListByClaims + (userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); assertEquals(3,list.size()); } @@ -457,7 +1062,7 @@ private void mockUserstoreManager() throws Exception { mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance).thenReturn( identityRecoveryServiceDataHolder); when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(realmService); - when(realmService.getTenantUserRealm(ArgumentMatchers.anyInt())).thenReturn(userRealm); + when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm); when(userRealm.getClaimManager()).thenReturn(claimManager); when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); } From 558fc6526ff7936c5cf0829501aadbfec2fe18cb Mon Sep 17 00:00:00 2001 From: kumuditha Date: Thu, 19 Sep 2024 15:08:37 +0530 Subject: [PATCH 07/14] Update comments --- .../internal/service/impl/UserAccountRecoveryManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index d5e8460222..5c63f357a6 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -359,7 +359,7 @@ public String getUsernameByClaims(Map claims, String tenantDomai } /** - * Get the user list for the given claims. + * Get the userlist for the given claims. * * @param claims List of UserClaims * @param tenantDomain Tenant domain From 8dd2de8f98cc8ad1b09f688d7d5bca30a7930a00 Mon Sep 17 00:00:00 2001 From: kumuditha Date: Fri, 20 Sep 2024 20:03:48 +0530 Subject: [PATCH 08/14] Add config to enable username recovery support for shared claims --- .../identity/recovery/IdentityRecoveryConstants.java | 1 + .../service/impl/UserAccountRecoveryManager.java | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index 1b4faf3df7..4580e7559d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -579,6 +579,7 @@ public static class ConnectorConfig { public static final String FORCE_ADD_PW_RECOVERY_QUESTION = "Recovery.Question.Password.Forced.Enable"; public static final String FORCE_MIN_NO_QUESTION_ANSWERED = "Recovery.Question.MinQuestionsToAnswer"; public static final String USERNAME_RECOVERY_ENABLE = "Recovery.Notification.Username.Enable"; + public static final String USERNAME_RECOVERY_SHARED_CLAIMS = "Recovery.Notification.Username.SharedClaims"; public static final String QUESTION_CHALLENGE_SEPARATOR = "Recovery.Question.Password.Separator"; public static final String QUESTION_MIN_NO_ANSWER = "Recovery.Question.Password.MinAnswers"; public static final String EXPIRY_TIME = "Recovery.ExpiryTime"; diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index 5c63f357a6..fb8f4ad605 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -496,9 +496,17 @@ private void populateUserListFromClaimsForDomain(int tenantId, Map 1 && !Boolean.parseBoolean(IdentityUtil.getProperty( + IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SHARED_CLAIMS))) { + log.warn("Multiple users matched for given claims set: " + claims.keySet()); + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS, null); + } } } From 02927e4a0d57bf990d7b980ee1dda6bbea61c425 Mon Sep 17 00:00:00 2001 From: kumuditha Date: Fri, 20 Sep 2024 20:25:10 +0530 Subject: [PATCH 09/14] Add unit tests for exception handling --- .../impl/UserAccountRecoveryManagerTest.java | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java index 27d45d467b..491c5ebaaa 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java @@ -1024,7 +1024,7 @@ private void testNoClaimsProvidedToRetrieveMatchingUsers() { } /** - * Test multiple users matching for the given set of claims error. + * Test multiple users matching for the given set of claims. * * @throws Exception Error while checking for matched users. */ @@ -1041,13 +1041,46 @@ private void testMultipleUsersMatchingForGivenClaims() throws Exception { IdentityEventService identityEventService = mock(IdentityEventService.class); when(IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService()).thenReturn(identityEventService); - + mockedIdentityUtil.when(() -> IdentityUtil.getProperty + (IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SHARED_CLAIMS)) + .thenReturn("true"); ArrayList list = userAccountRecoveryManager.getUserListByClaims (userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); assertEquals(3,list.size()); } + /** + * Test multiple users matching for the given set of claims error. + * + * @throws Exception Error while checking for matched users. + */ + @Test + private void testMultipleUsersMatchingForGivenClaimsException() throws Exception { + + mockUserstoreManager(); + try { + mockedUtils.when(() -> Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS, null)) + .thenReturn(IdentityException.error(IdentityRecoveryClientException.class, + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS.getCode(),"")); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class),anyString(),anyString(), + anyInt(),anyInt(),isNull(), isNull())).thenReturn(getFilteredUsers()); + when(claimManager.getAttributeName(anyString(),anyString())). + thenReturn("http://wso2.org/claims/mockedClaim"); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + String username = userAccountRecoveryManager + .getUsernameByClaims(userClaims, MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + assertNull(username, "UserAccountRecoveryManager: Exception should be thrown. Therefore, a " + + "value for an identified user cannot be returned : "); + } catch (IdentityRecoveryException e) { + assertEquals(e.getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS.getCode(), + "Invalid error code for existing multiple users for given set of claims"); + } + } /** * Get UserstoreManager by mocking IdentityRecoveryServiceDataHolder. From 517efa6e3d5b7b0c3c4601a0559d5885b74fb40d Mon Sep 17 00:00:00 2001 From: kumuditha Date: Thu, 26 Sep 2024 08:32:26 +0530 Subject: [PATCH 10/14] Change config variables --- .../carbon/identity/recovery/IdentityRecoveryConstants.java | 2 +- .../internal/service/impl/UserAccountRecoveryManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index 4580e7559d..c18c73f372 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -579,7 +579,7 @@ public static class ConnectorConfig { public static final String FORCE_ADD_PW_RECOVERY_QUESTION = "Recovery.Question.Password.Forced.Enable"; public static final String FORCE_MIN_NO_QUESTION_ANSWERED = "Recovery.Question.MinQuestionsToAnswer"; public static final String USERNAME_RECOVERY_ENABLE = "Recovery.Notification.Username.Enable"; - public static final String USERNAME_RECOVERY_SHARED_CLAIMS = "Recovery.Notification.Username.SharedClaims"; + public static final String USERNAME_RECOVERY_NON_UNIQUE_USERNAME = "Recovery.Notification.Username.NonUniqueUsername"; public static final String QUESTION_CHALLENGE_SEPARATOR = "Recovery.Question.Password.Separator"; public static final String QUESTION_MIN_NO_ANSWER = "Recovery.Question.Password.MinAnswers"; public static final String EXPIRY_TIME = "Recovery.ExpiryTime"; diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index fb8f4ad605..bda061f23c 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -502,7 +502,7 @@ private void populateUserListFromClaimsForDomain(int tenantId, Map 1 && !Boolean.parseBoolean(IdentityUtil.getProperty( - IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SHARED_CLAIMS))) { + IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_NON_UNIQUE_USERNAME))) { log.warn("Multiple users matched for given claims set: " + claims.keySet()); throw Utils.handleClientException( IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS, null); From c56ac16b88525c5f67fd74280b825ae1850b9a05 Mon Sep 17 00:00:00 2001 From: Kumuditha Date: Fri, 27 Sep 2024 09:43:23 +0530 Subject: [PATCH 11/14] Update userList creation logic --- .../service/impl/UserAccountRecoveryManager.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index bda061f23c..79f04f2748 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -73,6 +73,7 @@ import java.util.Map; import java.util.UUID; +import static java.lang.Integer.MAX_VALUE; import static org.wso2.carbon.identity.recovery.RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY; import static org.wso2.carbon.identity.recovery.RecoveryScenarios.QUESTION_BASED_PWD_RECOVERY; import static org.wso2.carbon.identity.recovery.RecoveryScenarios.USERNAME_RECOVERY; @@ -496,13 +497,15 @@ private void populateUserListFromClaimsForDomain(int tenantId, Map 1 && !Boolean.parseBoolean(IdentityUtil.getProperty( - IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_NON_UNIQUE_USERNAME))) { + if (userList.size() > 1 && !nonUniqueUsernameEnabled) { log.warn("Multiple users matched for given claims set: " + claims.keySet()); throw Utils.handleClientException( IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS, null); From 95cf2aba41a16ee494c57e92f3ed408b5a685e81 Mon Sep 17 00:00:00 2001 From: Kumuditha Date: Mon, 30 Sep 2024 23:05:43 +0530 Subject: [PATCH 12/14] Add unit tests for UsernameRecoveryManagerImpl --- .../username/UsernameRecoveryManagerImpl.java | 15 +- .../impl/UserAccountRecoveryManagerTest.java | 2 +- .../UsernameRecoveryManagerImplTest.java | 413 ++++++++++++++++++ .../src/test/resources/testng.xml | 1 + 4 files changed, 417 insertions(+), 14 deletions(-) create mode 100644 components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java index 2622d490bc..46c713ba40 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2020, WSO2 LLC. (http://www.wso2.org) * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -190,18 +190,7 @@ public UsernameRecoverDTO notify(String recoveryCode, String channelId, String t */ private boolean useLegacyAPIApproach(Map properties) { - if (MapUtils.isNotEmpty(properties)) { - try { - return Boolean.parseBoolean(properties.get(IdentityRecoveryConstants.USE_LEGACY_API_PROPERTY_KEY)); - } catch (NumberFormatException e) { - if (log.isDebugEnabled()) { - String message = String.format("Invalid boolean value : %s to enable legacyAPIs", properties - .get(IdentityRecoveryConstants.USE_LEGACY_API_PROPERTY_KEY)); - log.debug(message); - } - } - } - return false; + return Boolean.parseBoolean(properties.get(IdentityRecoveryConstants.USE_LEGACY_API_PROPERTY_KEY)); } /** diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java index 491c5ebaaa..285cb663d5 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java @@ -1042,7 +1042,7 @@ private void testMultipleUsersMatchingForGivenClaims() throws Exception { IdentityEventService identityEventService = mock(IdentityEventService.class); when(IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService()).thenReturn(identityEventService); mockedIdentityUtil.when(() -> IdentityUtil.getProperty - (IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SHARED_CLAIMS)) + (IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_NON_UNIQUE_USERNAME)) .thenReturn("true"); ArrayList list = userAccountRecoveryManager.getUserListByClaims diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java new file mode 100644 index 0000000000..637b212fe0 --- /dev/null +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wso2.carbon.identity.recovery.internal.service.impl.username; + +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.application.common.model.User; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; +import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; +import org.wso2.carbon.identity.recovery.IdentityRecoveryException; +import org.wso2.carbon.identity.recovery.RecoverySteps; +import org.wso2.carbon.identity.recovery.dto.RecoveryInformationDTO; +import org.wso2.carbon.identity.recovery.dto.UsernameRecoverDTO; +import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; +import org.wso2.carbon.identity.recovery.internal.service.impl.UserAccountRecoveryManager; +import org.wso2.carbon.identity.recovery.model.UserRecoveryData; +import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; +import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; +import org.wso2.carbon.identity.recovery.util.Utils; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; +import static org.testng.Assert.assertEquals; + +/** + * Test class for UsernameRecoveryManagerImpl. + */ +public class UsernameRecoveryManagerImplTest { + + private static final String TENANT_DOMAIN = "carbon.super"; + private static final String TRUE = "true"; + private static final String FALSE = "false"; + + @Mock + private UserAccountRecoveryManager mockUserAccountRecoveryManager; + + @Mock + private UserRecoveryData mockUserRecoveryData; + + @Mock + private IdentityRecoveryServiceDataHolder identityRecoveryServiceDataHolder; + + @Mock + private UserRecoveryDataStore mockUserRecoveryDataStore; + + @Mock + private IdentityEventService identityEventService; + + @InjectMocks + private UsernameRecoveryManagerImpl usernameRecoveryManager; + + private MockedStatic mockedUtils; + private MockedStatic mockedRecoveryManagerStatic; + private MockedStatic mockedJDBCRecoveryDataStore; + private MockedStatic mockedIdentityRecoveryServiceDataHolder; + private MockedStatic mockURLDecoder; + private MockedStatic mockedIdentityUtil; + + /** + * Set up the test environment. + */ + @BeforeMethod + public void setUp() { + + openMocks(this); + mockedUtils = mockStatic(Utils.class); + mockedRecoveryManagerStatic = mockStatic(UserAccountRecoveryManager.class); + mockedJDBCRecoveryDataStore = mockStatic(JDBCRecoveryDataStore.class); + mockedIdentityRecoveryServiceDataHolder = mockStatic(IdentityRecoveryServiceDataHolder.class); + mockURLDecoder = mockStatic(URLDecoder.class); + mockedIdentityUtil = mockStatic(IdentityUtil.class); + } + + /** + * Tear down the test environment. + */ + @AfterMethod + public void tearDown() { + + mockedUtils.close(); + mockedRecoveryManagerStatic.close(); + mockedJDBCRecoveryDataStore.close(); + mockedIdentityRecoveryServiceDataHolder.close(); + mockURLDecoder.close(); + mockedIdentityUtil.close(); + } + + /** + * Test to validate tenant domain. + * + * @throws IdentityRecoveryException if an error occurs during validation. + */ + @Test(expectedExceptions = IdentityRecoveryClientException.class) + public void testTenantDomainValidation() throws IdentityRecoveryException { + + when(Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_USERNAME_RECOVERY_EMPTY_TENANT_DOMAIN.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_USERNAME_RECOVERY_EMPTY_TENANT_DOMAIN.getMessage(), + null)).thenReturn(new IdentityRecoveryClientException(null)); + usernameRecoveryManager.initiate(null, null, null); + } + + /** + * Test to validate configurations. + * + * @throws IdentityRecoveryException if an error occurs during validation. + */ + @Test(expectedExceptions = IdentityRecoveryClientException.class) + public void testConfigValidation() throws IdentityRecoveryException { + + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(FALSE); + when(Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_USERNAME_RECOVERY_NOT_ENABLED, null)) + .thenReturn(new IdentityRecoveryClientException(null)); + usernameRecoveryManager.initiate(null, TENANT_DOMAIN, null); + } + + /** + * Data provider for channel ID. + * + * @return Object array containing channel IDs. + */ + @DataProvider + public Object[][] channelIDProvider() { + return new Object[][] { + { null }, + { "0" } + }; + } + + /** + * Test to validate channel ID exception. + * + * @param channelId the channel ID to validate. + * @throws IdentityRecoveryException if an error occurs during validation. + */ + @Test(dataProvider = "channelIDProvider", expectedExceptions = IdentityRecoveryClientException.class) + public void testChannelIDValidation(String channelId) throws IdentityRecoveryException { + + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", FALSE); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + when(mockUserAccountRecoveryManager.getUserListByClaims(null, TENANT_DOMAIN)).thenReturn(null); + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CHANNEL_ID, null)) + .thenReturn(new IdentityRecoveryClientException(null)); + usernameRecoveryManager.notify(null, channelId, TENANT_DOMAIN, properties); + } + + /** + * Test to invalidate recovery code. + * + * @throws IdentityRecoveryException if an error occurs during invalidation. + */ + @Test(expectedExceptions = NullPointerException.class) + public void testInvalidateRecoveryCode() throws IdentityRecoveryException { + + String recoveryCode = UUID.randomUUID().toString(); + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", FALSE); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) + .thenReturn(mockUserRecoveryData); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); + } + + /** + * Test to invalidate recovery code exception. + * + * @throws IdentityRecoveryException if an error occurs during invalidation. + */ + @Test(expectedExceptions = NullPointerException.class) + public void testInvalidateRecoveryCodeWithException() throws IdentityRecoveryException { + + String recoveryCode = UUID.randomUUID().toString(); + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", FALSE); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) + .thenReturn(mockUserRecoveryData); + when(mockUserRecoveryData.getRecoveryFlowId()).thenReturn("FlowID"); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); + } + + /** + * Test to extract notification channel details exception. + * + * @throws IdentityRecoveryException if an error occurs during extraction. + */ + @Test(expectedExceptions = IdentityRecoveryClientException.class) + public void testExtractChannelDetails() throws IdentityRecoveryException { + + String recoveryCode = UUID.randomUUID().toString(); + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", FALSE); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) + .thenReturn(mockUserRecoveryData); + when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("123"); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CHANNEL_ID, null)) + .thenReturn(new IdentityRecoveryClientException(null)); + usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); + } + + /** + * Test to notify user. + * + * @throws IdentityRecoveryException if an error occurs during notification. + */ + @Test + public void testNotifyUser() throws IdentityRecoveryException { + + String recoveryCode = UUID.randomUUID().toString(); + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", FALSE); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) + .thenReturn(mockUserRecoveryData); + when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("EXTERNAL,EXTERNAL"); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + User mockUser = new User(); + mockUser.setUserName("KD123"); + mockUser.setTenantDomain(TENANT_DOMAIN); + when(mockUserRecoveryData.getUser()).thenReturn(mockUser); + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CHANNEL_ID, null)) + .thenReturn(new IdentityRecoveryClientException(null)); + UsernameRecoverDTO code = usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); + assertEquals(code.getCode(), "UNR-02002"); + } + + /** + * Test to notify user with exception. + * + * @throws IdentityRecoveryException if an error occurs during notification. + */ + @Test + public void testNotifyUserException() throws IdentityRecoveryException { + + mockIdentityEventService(); + String recoveryCode = UUID.randomUUID().toString(); + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", FALSE); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) + .thenReturn(mockUserRecoveryData); + when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("SMS,SMS"); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + when(Utils.resolveEventName(anyString())).thenReturn("TRIGGER_SMS_NOTIFICATION_LOCAL"); + User mockUser = new User(); + mockUser.setUserName("KD123"); + mockUser.setTenantDomain(TENANT_DOMAIN); + when(mockUserRecoveryData.getUser()).thenReturn(mockUser); + UsernameRecoverDTO result = usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); + assertEquals(result.getCode(), "UNR-02001"); + assertEquals(result.getMessage(), "Username recovery information sent via user preferred notification channel."); + } + + /** + * Test to validate callback URL. + * + * @throws IdentityRecoveryException if an error occurs during validation. + */ + @Test + public void testCallbackURLValidation() throws IdentityRecoveryException { + + mockIdentityEventService(); + String callbackURL = "http://localhost:8080"; + String recoveryCode = UUID.randomUUID().toString(); + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", TRUE); + properties.put(IdentityRecoveryConstants.CALLBACK, callbackURL); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) + .thenReturn(mockUserRecoveryData); + when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("SMS,SMS"); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + when(Utils.resolveEventName(anyString())).thenReturn("TRIGGER_SMS_NOTIFICATION_LOCAL"); + User mockUser = new User(); + mockUser.setUserName("KD123"); + mockUser.setTenantDomain(TENANT_DOMAIN); + when(mockUserRecoveryData.getUser()).thenReturn(mockUser); + when(Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_CALLBACK_URL_NOT_VALID, callbackURL)) + .thenReturn(new IdentityRecoveryClientException(null)); + usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); + } + + /** + * Test to get callback URL with exception. + * + * @throws IdentityRecoveryException if an error occurs during retrieval. + */ + @Test(expectedExceptions = NullPointerException.class) + public void testCallbackURLDecoding() throws IdentityRecoveryException { + + mockIdentityEventService(); + String callbackURL = "http://localhost:8080"; + String recoveryCode = UUID.randomUUID().toString(); + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", TRUE); + properties.put(IdentityRecoveryConstants.CALLBACK, callbackURL); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) + .thenReturn(mockUserRecoveryData); + when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("SMS,SMS"); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + when(Utils.resolveEventName(anyString())).thenReturn("TRIGGER_SMS_NOTIFICATION_LOCAL"); + mockURLDecoder.when(() -> URLDecoder.decode(anyString(), anyString())).thenThrow(new UnsupportedEncodingException()); + usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); + } + + /** + * Test to initiate recovery if username is null. + * + * @throws IdentityRecoveryException if an error occurs during initiation. + */ + @Test(expectedExceptions = IdentityRecoveryClientException.class) + public void testInitiateRecoveryWithNullUsername() throws IdentityRecoveryException { + + org.wso2.carbon.user.core.common.User testUser = mock(org.wso2.carbon.user.core.common.User.class); + testUser.setUserID("123"); + testUser.setTenantDomain(TENANT_DOMAIN); + testUser.setUsername("testUser"); + ArrayList userList = new ArrayList<>(); + userList.add(testUser); + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", TRUE); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.getUserListByClaims(null, TENANT_DOMAIN)).thenReturn(userList); + when(IdentityUtil.getProperty(anyString())).thenReturn(TRUE); + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null)) + .thenReturn(new IdentityRecoveryClientException(null)); + usernameRecoveryManager.initiate(null, TENANT_DOMAIN, properties); + } + + /** + * Test to initiate recovery if username is null and return a valid result. + * + * @throws IdentityRecoveryException if an error occurs during initiation. + */ + @Test + public void testInitiateRecoveryValidUsername() throws IdentityRecoveryException { + + mockIdentityEventService(); + org.wso2.carbon.user.core.common.User testUser = mock(org.wso2.carbon.user.core.common.User.class); + when(testUser.getDomainQualifiedUsername()).thenReturn("testUser"); + ArrayList userList = new ArrayList<>(); + userList.add(testUser); + Map properties = new HashMap<>(); + properties.put("useLegacyAPI", TRUE); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.getUserListByClaims(null, TENANT_DOMAIN)).thenReturn(userList); + when(IdentityUtil.getProperty(anyString())).thenReturn(TRUE); + when(Utils.isNotificationsInternallyManaged(TENANT_DOMAIN, properties)).thenReturn(true); + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null)) + .thenReturn(new IdentityRecoveryClientException(null)); + RecoveryInformationDTO result = usernameRecoveryManager.initiate(null, TENANT_DOMAIN, properties); + assertEquals(result.getUsername(), "testUser"); + } + + /** + * Mock the IdentityEventService. + */ + private void mockIdentityEventService() { + + mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance).thenReturn( + identityRecoveryServiceDataHolder); + when(identityRecoveryServiceDataHolder.getIdentityEventService()).thenReturn(identityEventService); + } +} \ No newline at end of file diff --git a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml index 7c8f7107ff..645c37e12d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml +++ b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml @@ -27,6 +27,7 @@ + From bc112f335585f530d11d09da553a12f9d8d52729 Mon Sep 17 00:00:00 2001 From: Kumuditha Date: Tue, 1 Oct 2024 16:17:51 +0530 Subject: [PATCH 13/14] Improve test coverage --- .../impl/UserAccountRecoveryManagerTest.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java index 285cb663d5..46daf5e741 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java @@ -50,6 +50,7 @@ import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; +import org.wso2.carbon.user.api.RealmConfiguration; import org.wso2.carbon.user.api.UserRealm; import org.wso2.carbon.user.core.UserStoreException; import org.wso2.carbon.user.core.claim.ClaimManager; @@ -62,6 +63,7 @@ import java.util.HashMap; import java.util.List; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.anyString; @@ -729,6 +731,78 @@ public Object[][] exceptionScenarios() { }; } + /** + * Tests that an IdentityRecoveryServerException is thrown during GetClaimListOfUser + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test (expectedExceptions = IdentityRecoveryServerException.class) + public void testGetClaimListOfUserException() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + mockedUtils.when(() -> Utils.isNotificationsInternallyManaged(anyString(), any())) + .thenReturn(true); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(getOneFilteredUser()); + when(claimManager.getAttributeName(anyString(), anyString())) + .thenReturn("http://wso2.org/claims/mockedClaim"); + mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY"); + when(abstractUserStoreManager.getUserClaimValues(anyString(), any(String[].class), isNull())) + .thenThrow(new UserStoreException("Simulated exception")); + HashMap properties = new HashMap<>(); + properties.put(IdentityEventConstants.EventProperty.USER, "user"); + when(IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService()) + .thenReturn(identityEventService); + doThrow(new IdentityEventException("error")) + .when(identityEventService).handleEvent(any(Event.class)); + when(Utils.handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_ERROR_LOADING_USER_CLAIMS, null)) + .thenReturn(new IdentityRecoveryServerException(null, null, null)); + userAccountRecoveryManager.retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, + RecoveryScenarios.USERNAME_RECOVERY, properties); + } + + /** + * Tests testGetDomainNames through retrieveUserRecoveryInformation + * + * @throws Exception if there is an issue with mocking or method invocation. + */ + @Test(expectedExceptions = NullPointerException.class) + public void testGetDomainNames() throws Exception { + + mockUserstoreManager(); + mockBuildUser(); + mockedUtils.when(() -> Utils.isNotificationsInternallyManaged(anyString(), any())) + .thenReturn(true); + when(identityRecoveryServiceDataHolder.getMultiAttributeLoginService()) + .thenReturn(multiAttributeLoginService); + when(multiAttributeLoginService.isEnabled(anyString())).thenReturn(false); + when(abstractUserStoreManager.getUserListWithID(any(Condition.class), anyString(), anyString(), + anyInt(), anyInt(), isNull(), isNull())).thenReturn(getOneFilteredUser()); + when(claimManager.getAttributeName(anyString(), anyString())) + .thenReturn("http://wso2.org/claims/mockedClaim"); + RealmConfiguration realmConfiguration = mock(RealmConfiguration.class); + when(realmConfiguration.getUserStoreProperty("DomainName")).thenReturn("PRIMARY"); + mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY"); + when(abstractUserStoreManager.getRealmConfiguration()).thenReturn(realmConfiguration); + when(Utils.handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_MATCHING_USERS, null)) + .thenReturn(new IdentityRecoveryServerException(null, null, null)); + + AtomicInteger callCount = new AtomicInteger(0); + when(abstractUserStoreManager.getSecondaryUserStoreManager()).thenAnswer(invocation -> { + if (callCount.incrementAndGet() == 1) { + return abstractUserStoreManager; + } + return null; + }); + + userAccountRecoveryManager.retrieveUserRecoveryInformation(userClaims, StringUtils.EMPTY, + RecoveryScenarios.USERNAME_RECOVERY, null); + } + /** * Tests that an IdentityRecoveryException is thrown when loading user recovery data * from a recovery flow ID and different error codes are simulated. From e7b697eb379fa31c0c7362ba1c9b666b9d38e5cd Mon Sep 17 00:00:00 2001 From: Kumuditha Date: Tue, 1 Oct 2024 18:02:08 +0530 Subject: [PATCH 14/14] Address review comments --- .../service/impl/username/UsernameRecoveryManagerImplTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java index 637b212fe0..d12714ab79 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java @@ -410,4 +410,4 @@ private void mockIdentityEventService() { identityRecoveryServiceDataHolder); when(identityRecoveryServiceDataHolder.getIdentityEventService()).thenReturn(identityEventService); } -} \ No newline at end of file +}