diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/FlowExecutionTestBase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/FlowExecutionTestBase.java index c42ba694cca..6bad7ad0cb5 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/FlowExecutionTestBase.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/FlowExecutionTestBase.java @@ -42,6 +42,11 @@ public class FlowExecutionTestBase extends RESTAPIServerTestBase { protected static final String TYPE_VIEW = "VIEW"; protected static final String TYPE_REDIRECTION = "REDIRECTION"; protected static final String REGISTRATION_FLOW = "registration-flow.json"; + protected static final String PASSWORD_RECOVERY_FLOW = "password-recovery-flow.json"; + protected static final String PASSWORD_RECOVERY_MULTI_ATTRIBUTE_FLOW = + "password-recovery-multi-attribute-flow.json"; + protected static final String PASSWORD_RECOVERY_CONTROLS_DISABLED_FLOW = + "password-recovery-controls-disabled-flow.json"; protected static final String API_DEFINITION_NAME = "flow-execution.yaml"; protected static final String API_VERSION = "v1"; protected static final String API_PACKAGE_NAME = "org.wso2.carbon.identity.api.server.flow.execution.v1"; @@ -82,6 +87,46 @@ protected void addRegistrationFlow(FlowManagementClient client) throws Exception client.putFlow(flowRequest); } + protected void addPasswordRecoveryFlow(FlowManagementClient client) throws Exception { + + String passwordRecoveryFlowRequestJson = readResource(PASSWORD_RECOVERY_FLOW); + FlowRequest flowRequest = new ObjectMapper() + .readValue(passwordRecoveryFlowRequestJson, FlowRequest.class); + client.putFlow(flowRequest); + } + + protected void addPasswordRecoveryMultiAttributeFlow(FlowManagementClient client) throws Exception { + + String passwordRecoveryFlowRequestJson = readResource(PASSWORD_RECOVERY_MULTI_ATTRIBUTE_FLOW); + FlowRequest flowRequest = new ObjectMapper() + .readValue(passwordRecoveryFlowRequestJson, FlowRequest.class); + client.putFlow(flowRequest); + } + + protected void addPasswordRecoveryControlsDisabledFlow(FlowManagementClient client) throws Exception { + + String passwordRecoveryFlowRequestJson = readResource(PASSWORD_RECOVERY_CONTROLS_DISABLED_FLOW); + FlowRequest flowRequest = new ObjectMapper() + .readValue(passwordRecoveryFlowRequestJson, FlowRequest.class); + client.putFlow(flowRequest); + } + + protected void enablePasswordRecoveryFlow(FlowManagementClient client) throws Exception { + + FlowConfig flowConfigDTO = new FlowConfig(); + flowConfigDTO.setIsEnabled(true); + flowConfigDTO.setFlowType(FlowTypes.PASSWORD_RECOVERY); + client.updateFlowConfig(flowConfigDTO); + } + + protected void disablePasswordRecoveryFlow(FlowManagementClient client) throws Exception { + + FlowConfig flowConfigDTO = new FlowConfig(); + flowConfigDTO.setIsEnabled(false); + flowConfigDTO.setFlowType(FlowTypes.PASSWORD_RECOVERY); + client.updateFlowConfig(flowConfigDTO); + } + protected void enableFlow(String flowType, FlowManagementClient client) throws Exception { FlowConfig flowConfigDTO = new FlowConfig(); @@ -105,5 +150,6 @@ protected void disableFlow(String flowType, FlowManagementClient client) throws protected static class FlowTypes { public static final String REGISTRATION = "REGISTRATION"; + public static final String PASSWORD_RECOVERY = "PASSWORD_RECOVERY"; } } diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/RecoveryEnumerationControlsExecutionNegativeTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/RecoveryEnumerationControlsExecutionNegativeTest.java new file mode 100644 index 00000000000..16bc68d9079 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/RecoveryEnumerationControlsExecutionNegativeTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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.identity.integration.test.rest.api.server.flow.execution.v1; + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; +import org.wso2.carbon.automation.engine.context.TestUserMode; +import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.FlowExecutionRequest; +import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.FlowExecutionResponse; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.Error; +import org.wso2.identity.integration.test.restclients.FlowExecutionClient; +import org.wso2.identity.integration.test.restclients.FlowManagementClient; + +import java.util.HashMap; +import java.util.Map; + +import static org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.FlowExecutionTestBase.FlowTypes.PASSWORD_RECOVERY; + +/** + * Negative tests for the password recovery flow execution with the user enumeration and account status controls. + */ +public class RecoveryEnumerationControlsExecutionNegativeTest extends FlowExecutionTestBase { + + private static final String USERNAME_CLAIM = "http://wso2.org/claims/username"; + private static final String USER_RESOLVE_ACTION_ID = "button_1ov4"; + private static final String TEST_IDENTIFIER = "PwdRecNegTestUser"; + + private static final String FLOW_NOT_ENABLED_ON_INITIATE = "FE-60101"; + private static final String FLOW_NOT_ENABLED_OR_INVALID_FLOW_ID = "FE-60001"; + private static final String INVALID_INPUTS = "FE-60008"; + + private FlowExecutionClient flowExecutionClient; + private FlowManagementClient flowManagementClient; + private static String flowId; + + @DataProvider(name = "restAPIUserConfigProvider") + public static Object[][] restAPIUserConfigProvider() { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_ADMIN}, + {TestUserMode.TENANT_ADMIN} + }; + } + + @Factory(dataProvider = "restAPIUserConfigProvider") + public RecoveryEnumerationControlsExecutionNegativeTest(TestUserMode userMode) throws Exception { + + super.init(userMode); + this.context = isServer; + this.authenticatingUserName = context.getContextTenant().getTenantAdmin().getUserName(); + this.authenticatingCredential = context.getContextTenant().getTenantAdmin().getPassword(); + this.tenant = context.getContextTenant().getDomain(); + } + + @BeforeClass(alwaysRun = true) + public void setupClass() throws Exception { + + super.testInit(API_VERSION, swaggerDefinition, tenantInfo.getDomain()); + flowExecutionClient = new FlowExecutionClient(serverURL, tenantInfo); + flowManagementClient = new FlowManagementClient(serverURL, tenantInfo); + addPasswordRecoveryFlow(flowManagementClient); + } + + @AfterClass(alwaysRun = true) + public void tearDownClass() throws Exception { + + disablePasswordRecoveryFlow(flowManagementClient); + flowExecutionClient.closeHttpClient(); + flowManagementClient.closeHttpClient(); + } + + @Test(description = "Initiating the password recovery flow before it is enabled returns FE-60101.") + public void testInitiateFlowWithoutEnable() throws Exception { + + Object responseObj = flowExecutionClient.initiateFlowExecution(PASSWORD_RECOVERY); + Assert.assertTrue(responseObj instanceof Error); + Assert.assertEquals(((Error) responseObj).getCode(), FLOW_NOT_ENABLED_ON_INITIATE); + } + + @Test(description = "Executing the password recovery flow before it is enabled returns FE-60001.", + dependsOnMethods = "testInitiateFlowWithoutEnable") + public void testExecuteFlowWithoutEnable() throws Exception { + + Object responseObj = flowExecutionClient.executeFlow(getRequest("INVALID_FLOW_ID", defaultInputs())); + Assert.assertTrue(responseObj instanceof Error); + Assert.assertEquals(((Error) responseObj).getCode(), FLOW_NOT_ENABLED_OR_INVALID_FLOW_ID); + } + + @Test(description = "Enable the flow and initiate it to obtain a valid flow id.", + dependsOnMethods = "testExecuteFlowWithoutEnable") + public void testInitiateFlow() throws Exception { + + enablePasswordRecoveryFlow(flowManagementClient); + Object responseObj = flowExecutionClient.initiateFlowExecution(PASSWORD_RECOVERY); + Assert.assertTrue(responseObj instanceof FlowExecutionResponse); + FlowExecutionResponse response = (FlowExecutionResponse) responseObj; + Assert.assertNotNull(response.getFlowId()); + Assert.assertEquals(response.getFlowStatus(), STATUS_INCOMPLETE); + Assert.assertEquals(response.getType().toString(), TYPE_VIEW); + flowId = response.getFlowId(); + } + + @Test(description = "Executing with an invalid flow id returns FE-60001.", + dependsOnMethods = "testInitiateFlow") + public void testExecuteFlowWithInvalidFlowId() throws Exception { + + Object responseObj = flowExecutionClient.executeFlow(getRequest("INVALID_FLOW_ID", defaultInputs())); + Assert.assertTrue(responseObj instanceof Error); + Assert.assertEquals(((Error) responseObj).getCode(), FLOW_NOT_ENABLED_OR_INVALID_FLOW_ID); + } + + @Test(description = "Executing with empty inputs returns FE-60008.", + dependsOnMethods = "testExecuteFlowWithInvalidFlowId") + public void testExecuteFlowWithEmptyInputs() throws Exception { + + Object responseObj = flowExecutionClient.executeFlow(getRequest(flowId, new HashMap<>())); + Assert.assertTrue(responseObj instanceof Error); + Assert.assertEquals(((Error) responseObj).getCode(), INVALID_INPUTS); + } + + private static Map defaultInputs() { + + Map inputs = new HashMap<>(); + inputs.put(USERNAME_CLAIM, TEST_IDENTIFIER); + return inputs; + } + + private static FlowExecutionRequest getRequest(String flowId, Map inputs) { + + FlowExecutionRequest request = new FlowExecutionRequest(); + request.setFlowType(PASSWORD_RECOVERY); + request.setFlowId(flowId); + request.setActionId(USER_RESOLVE_ACTION_ID); + request.setInputs(inputs); + return request; + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/RecoveryEnumerationControlsExecutionPositiveTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/RecoveryEnumerationControlsExecutionPositiveTest.java new file mode 100644 index 00000000000..ea5ca191019 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/RecoveryEnumerationControlsExecutionPositiveTest.java @@ -0,0 +1,473 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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.identity.integration.test.rest.api.server.flow.execution.v1; + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; +import org.wso2.carbon.automation.engine.context.TestUserMode; +import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.Component; +import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.FlowExecutionRequest; +import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.FlowExecutionResponse; +import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.Message; +import org.wso2.identity.integration.test.rest.api.server.identity.governance.v1.dto.ConnectorsPatchReq; +import org.wso2.identity.integration.test.rest.api.server.identity.governance.v1.dto.PropertyReq; +import org.wso2.identity.integration.test.rest.api.user.common.model.Email; +import org.wso2.identity.integration.test.rest.api.user.common.model.Name; +import org.wso2.identity.integration.test.rest.api.user.common.model.PatchOperationRequestObject; +import org.wso2.identity.integration.test.rest.api.user.common.model.UserItemAddGroupobj; +import org.wso2.identity.integration.test.rest.api.user.common.model.UserObject; +import org.wso2.identity.integration.test.restclients.FlowExecutionClient; +import org.wso2.identity.integration.test.restclients.FlowManagementClient; +import org.wso2.identity.integration.test.restclients.IdentityGovernanceRestClient; +import org.wso2.identity.integration.test.restclients.SCIM2RestClient; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.FlowExecutionTestBase.FlowTypes.PASSWORD_RECOVERY; + +/** + * Tests the user enumeration and account status controls of the password recovery flow execution. + */ +public class RecoveryEnumerationControlsExecutionPositiveTest extends FlowExecutionTestBase { + + private static final String USERNAME_CLAIM = "http://wso2.org/claims/username"; + // Generic identifier field used by the multi-attribute-login flow variant (resolved against the allowed + // attributes by the UserResolveExecutor), instead of binding the field to the username claim. + private static final String USER_IDENTIFIER_FIELD = "userIdentifier"; + private static final String USER_RESOLVE_ACTION_ID = "button_1ov4"; + + private static final String USER_SYSTEM_SCHEMA_ATTRIBUTE = "urn:scim:wso2:schema"; + private static final String ACCOUNT_LOCKED_ATTRIBUTE = "accountLocked"; + private static final String ACCOUNT_DISABLED_ATTRIBUTE = "accountDisabled"; + + private static final String LOCKED_USER = "PwdRecLockedUser"; + private static final String LOCKED_USER_EMAIL = "pwd.rec.locked@example.com"; + private static final String DISABLED_USER = "PwdRecDisabledUser"; + private static final String DISABLED_USER_EMAIL = "pwd.rec.disabled@example.com"; + private static final String NON_EXISTENT_USER = "PwdRecNonExistentUser"; + private static final String NON_EXISTENT_USER_EMAIL = "pwd.rec.nonexistent@example.com"; + private static final String ACTIVE_USER = "PwdRecActiveUser"; + private static final String ACTIVE_USER_EMAIL = "pwd.rec.active@example.com"; + private static final String TEST_USER_PASSWORD = "Wso2@Test123"; + + // The executor returns i18n keys wrapped in double braces (e.g. "{{user.not.found}}"); the framework + // serializes them verbatim, so the expected values must include the braces. + private static final String USER_NOT_FOUND_I18N_KEY = "{{user.not.found}}"; + private static final String ACCOUNT_LOCKED_I18N_KEY_PREFIX = "{{account.locked"; + private static final String ACCOUNT_DISABLED_I18N_KEY = "{{account.disabled}}"; + + // Account Management category; multi-attribute login handler connector and its properties. + private static final String ACCOUNT_MGT_CATEGORY = "QWNjb3VudCBNYW5hZ2VtZW50"; + private static final String MULTI_ATTRIBUTE_CONNECTOR_ID = "bXVsdGlhdHRyaWJ1dGUubG9naW4uaGFuZGxlcg"; + private static final String MULTI_ATTRIBUTE_ENABLE_PROPERTY = "account.multiattributelogin.handler.enable"; + private static final String MULTI_ATTRIBUTE_ALLOWED_ATTRIBUTES_PROPERTY = + "account.multiattributelogin.handler.allowedattributes"; + private static final String MULTI_ATTRIBUTE_ALLOWED_ATTRIBUTES = + "http://wso2.org/claims/username, http://wso2.org/claims/emailaddress"; + + private FlowExecutionClient flowExecutionClient; + private FlowManagementClient flowManagementClient; + private IdentityGovernanceRestClient identityGovernanceRestClient; + private SCIM2RestClient scim2RestClient; + + private String lockedUserId; + private String disabledUserId; + private String activeUserId; + + @DataProvider(name = "restAPIUserConfigProvider") + public static Object[][] restAPIUserConfigProvider() { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_ADMIN}, + {TestUserMode.TENANT_ADMIN} + }; + } + + @Factory(dataProvider = "restAPIUserConfigProvider") + public RecoveryEnumerationControlsExecutionPositiveTest(TestUserMode userMode) throws Exception { + + super.init(userMode); + this.context = isServer; + this.authenticatingUserName = context.getContextTenant().getTenantAdmin().getUserName(); + this.authenticatingCredential = context.getContextTenant().getTenantAdmin().getPassword(); + this.tenant = context.getContextTenant().getDomain(); + } + + @BeforeClass(alwaysRun = true) + public void setupClass() throws Exception { + + super.testInit(API_VERSION, swaggerDefinition, tenantInfo.getDomain()); + flowExecutionClient = new FlowExecutionClient(serverURL, tenantInfo); + flowManagementClient = new FlowManagementClient(serverURL, tenantInfo); + identityGovernanceRestClient = new IdentityGovernanceRestClient(serverURL, tenantInfo); + scim2RestClient = new SCIM2RestClient(serverURL, tenantInfo); + + updatePasswordRecoveryFeatureStatus(true); + // The account disable handler is off by default; it must be enabled for the accountDisabled claim to be + // honoured by the UserResolveExecutor's account status check. + setAccountDisableHandlerStatus(true); + addPasswordRecoveryFlow(flowManagementClient); + enablePasswordRecoveryFlow(flowManagementClient); + + lockedUserId = createUser(LOCKED_USER, LOCKED_USER_EMAIL); + setAccountState(lockedUserId, ACCOUNT_LOCKED_ATTRIBUTE, true); + + disabledUserId = createUser(DISABLED_USER, DISABLED_USER_EMAIL); + setAccountState(disabledUserId, ACCOUNT_DISABLED_ATTRIBUTE, true); + + activeUserId = createUser(ACTIVE_USER, ACTIVE_USER_EMAIL); + } + + @AfterClass(alwaysRun = true) + public void tearDownClass() throws Exception { + + if (lockedUserId != null) { + scim2RestClient.deleteUser(lockedUserId); + } + if (disabledUserId != null) { + scim2RestClient.deleteUser(disabledUserId); + } + if (activeUserId != null) { + scim2RestClient.deleteUser(activeUserId); + } + setMultiAttributeLoginStatus(false); + disablePasswordRecoveryFlow(flowManagementClient); + updatePasswordRecoveryFeatureStatus(false); + setAccountDisableHandlerStatus(false); + identityGovernanceRestClient.closeHttpClient(); + flowExecutionClient.closeHttpClient(); + flowManagementClient.closeHttpClient(); + scim2RestClient.closeHttpClient(); + } + + @Test(description = "Initiate the password recovery flow and verify the username collection step is rendered.") + public void testInitiatePasswordRecoveryFlow() throws Exception { + + Object responseObj = flowExecutionClient.initiateFlowExecution(PASSWORD_RECOVERY); + Assert.assertTrue(responseObj instanceof FlowExecutionResponse); + FlowExecutionResponse response = (FlowExecutionResponse) responseObj; + Assert.assertNotNull(response.getFlowId()); + Assert.assertEquals(response.getFlowStatus(), STATUS_INCOMPLETE); + Assert.assertEquals(response.getType().toString(), TYPE_VIEW); + } + + @Test(description = "Submit a non-existent identifier with notifyUserExistence enabled and expect an error.", + dependsOnMethods = "testInitiatePasswordRecoveryFlow") + public void testNonExistentIdentifierIsNotified() throws Exception { + + FlowExecutionResponse response = submitIdentifier(NON_EXISTENT_USER); + assertErrorMessage(response, USER_NOT_FOUND_I18N_KEY); + } + + @Test(description = "Submit a locked user with notifyUserAccountStatus enabled and expect an error.", + dependsOnMethods = "testNonExistentIdentifierIsNotified") + public void testLockedAccountIsNotified() throws Exception { + + FlowExecutionResponse response = submitIdentifier(LOCKED_USER); + assertErrorMessage(response, ACCOUNT_LOCKED_I18N_KEY_PREFIX); + } + + @Test(description = "Submit a disabled user with notifyUserAccountStatus enabled and expect an error.", + dependsOnMethods = "testLockedAccountIsNotified") + public void testDisabledAccountIsNotified() throws Exception { + + FlowExecutionResponse response = submitIdentifier(DISABLED_USER); + assertErrorMessage(response, ACCOUNT_DISABLED_I18N_KEY); + } + + @Test(description = "Submit a valid active user with the controls enabled and expect the flow to advance " + + "past user resolution without any enumeration or account-status error.", + dependsOnMethods = "testDisabledAccountIsNotified") + public void testActiveUserResolvesAndAdvances() throws Exception { + + FlowExecutionResponse response = submitIdentifier(ACTIVE_USER); + + // Flow continues as a VIEW step (the next step is rendered), not terminated. + Assert.assertEquals(response.getFlowStatus(), STATUS_INCOMPLETE); + Assert.assertEquals(response.getType().toString(), TYPE_VIEW); + + // No enumeration / account-status ERROR was surfaced for a valid active user. + Assert.assertFalse(hasErrorMessage(response), + "Unexpected ERROR message for a valid active user: " + response.getData().getMessages()); + + // The flow advanced to the OTP verification step (proves it moved past user resolution). + Assert.assertTrue(containsOtpComponent(response.getData().getComponents()), + "Expected the OTP verification step to be rendered after resolving a valid user."); + } + + @Test(description = "Enable multi-attribute login (username + email) and persist the recovery flow variant whose " + + "identifier field uses the generic userIdentifier, so the recovery identifier can be an email.", + dependsOnMethods = "testActiveUserResolvesAndAdvances") + public void testEnableMultiAttributeLogin() throws Exception { + + setMultiAttributeLoginStatus(true); + addPasswordRecoveryMultiAttributeFlow(flowManagementClient); + } + + @Test(description = "With multi-attribute login enabled, submit a non-existent email and expect a user-not-found " + + "error.", dependsOnMethods = "testEnableMultiAttributeLogin") + public void testNonExistentEmailIsNotifiedWithMultiAttributeLogin() throws Exception { + + FlowExecutionResponse response = submitIdentifier(USER_IDENTIFIER_FIELD, NON_EXISTENT_USER_EMAIL); + assertErrorMessage(response, USER_NOT_FOUND_I18N_KEY); + } + + @Test(description = "With multi-attribute login enabled, submit a locked user's email and expect an error.", + dependsOnMethods = "testNonExistentEmailIsNotifiedWithMultiAttributeLogin") + public void testLockedAccountIsNotifiedWithMultiAttributeLogin() throws Exception { + + FlowExecutionResponse response = submitIdentifier(USER_IDENTIFIER_FIELD, LOCKED_USER_EMAIL); + assertErrorMessage(response, ACCOUNT_LOCKED_I18N_KEY_PREFIX); + } + + @Test(description = "With multi-attribute login enabled, submit a disabled user's email and expect an error.", + dependsOnMethods = "testLockedAccountIsNotifiedWithMultiAttributeLogin") + public void testDisabledAccountIsNotifiedWithMultiAttributeLogin() throws Exception { + + FlowExecutionResponse response = submitIdentifier(USER_IDENTIFIER_FIELD, DISABLED_USER_EMAIL); + assertErrorMessage(response, ACCOUNT_DISABLED_I18N_KEY); + } + + @Test(description = "With multi-attribute login enabled, submit a valid active user's email and expect the flow " + + "to advance past user resolution without any enumeration or account-status error.", + dependsOnMethods = "testDisabledAccountIsNotifiedWithMultiAttributeLogin") + public void testActiveUserResolvesViaEmailAndAdvances() throws Exception { + + FlowExecutionResponse response = submitIdentifier(USER_IDENTIFIER_FIELD, ACTIVE_USER_EMAIL); + + // Flow continues as a VIEW step (the next step is rendered), not terminated. + Assert.assertEquals(response.getFlowStatus(), STATUS_INCOMPLETE); + Assert.assertEquals(response.getType().toString(), TYPE_VIEW); + + // No enumeration / account-status ERROR was surfaced for a valid active user resolved via email. + Assert.assertFalse(hasErrorMessage(response), + "Unexpected ERROR message for a valid active user: " + response.getData().getMessages()); + + // The flow advanced to the OTP verification step (proves it moved past user resolution). + Assert.assertTrue(containsOtpComponent(response.getData().getComponents()), + "Expected the OTP verification step to be rendered after resolving a valid user via email."); + } + + @Test(description = "Disable the enumeration controls by persisting the controls-disabled flow (and disabling " + + "multi-attribute login) so the executor must silently advance instead of notifying the user.", + dependsOnMethods = "testActiveUserResolvesViaEmailAndAdvances") + public void testDisableEnumerationControls() throws Exception { + + setMultiAttributeLoginStatus(false); + addPasswordRecoveryControlsDisabledFlow(flowManagementClient); + } + + @Test(description = "With the controls disabled, submitting a non-existent identifier must silently advance the " + + "flow without any enumeration error.", dependsOnMethods = "testDisableEnumerationControls") + public void testNonExistentIdentifierSilentlyAdvances() throws Exception { + + FlowExecutionResponse response = submitIdentifier(NON_EXISTENT_USER); + assertSilentlyAdvances(response); + } + + @Test(description = "With the controls disabled, submitting a locked user must silently advance the flow without " + + "any account-status error.", dependsOnMethods = "testNonExistentIdentifierSilentlyAdvances") + public void testLockedAccountSilentlyAdvances() throws Exception { + + FlowExecutionResponse response = submitIdentifier(LOCKED_USER); + assertSilentlyAdvances(response); + } + + @Test(description = "With the controls disabled, submitting a disabled user must silently advance the flow " + + "without any account-status error.", dependsOnMethods = "testLockedAccountSilentlyAdvances") + public void testDisabledAccountSilentlyAdvances() throws Exception { + + FlowExecutionResponse response = submitIdentifier(DISABLED_USER); + assertSilentlyAdvances(response); + } + + /** + * Initiate a fresh password recovery flow and submit the given username under the username claim field. + * + * @param username Username submitted to the {@code UserResolveExecutor}. + * @return The flow execution response of the user resolution step. + * @throws Exception If the flow could not be initiated or executed. + */ + private FlowExecutionResponse submitIdentifier(String username) throws Exception { + + return submitIdentifier(USERNAME_CLAIM, username); + } + + /** + * Initiate a fresh password recovery flow and submit the given identifier under the given input field at the + * user resolution step. The field is the username claim for the default flow and {@code userIdentifier} for the + * multi-attribute-login variant. + * + * @param identifierField Input field identifier under which the value is submitted (username claim or + * {@code userIdentifier}). + * @param identifier Identifier value submitted to the {@code UserResolveExecutor}. + * @return The flow execution response of the user resolution step. + * @throws Exception If the flow could not be initiated or executed. + */ + private FlowExecutionResponse submitIdentifier(String identifierField, String identifier) throws Exception { + + Object initiationObj = flowExecutionClient.initiateFlowExecution(PASSWORD_RECOVERY); + Assert.assertTrue(initiationObj instanceof FlowExecutionResponse); + String flowId = ((FlowExecutionResponse) initiationObj).getFlowId(); + + FlowExecutionRequest request = new FlowExecutionRequest(); + request.setFlowType(PASSWORD_RECOVERY); + request.setFlowId(flowId); + request.setActionId(USER_RESOLVE_ACTION_ID); + Map inputs = new HashMap<>(); + inputs.put(identifierField, identifier); + request.setInputs(inputs); + + Object responseObj = flowExecutionClient.executeFlow(request); + Assert.assertTrue(responseObj instanceof FlowExecutionResponse); + return (FlowExecutionResponse) responseObj; + } + + /** + * Assert that the response re-prompts the same step and carries an ERROR message with the expected i18n key. + * The i18n key is matched as a prefix to tolerate reason-specific variants (e.g. account locked reasons). + */ + private void assertErrorMessage(FlowExecutionResponse response, String expectedI18nKeyPrefix) { + + Assert.assertEquals(response.getFlowStatus(), STATUS_INCOMPLETE); + Assert.assertEquals(response.getType().toString(), TYPE_VIEW); + + List messages = response.getData().getMessages(); + boolean hasExpectedError = messages != null && messages.stream().anyMatch(message -> + message.getType() == Message.TypeEnum.ERROR + && message.getI18nKey() != null + && message.getI18nKey().startsWith(expectedI18nKeyPrefix)); + Assert.assertTrue(hasExpectedError, + "No ERROR message with i18n key '" + expectedI18nKeyPrefix + "' in: " + messages); + } + + /** + * Assert that the flow silently advanced past user resolution: it re-renders as a VIEW step, carries no ERROR + * message, and renders the OTP verification step (the same response a valid user would get, so the caller + * cannot tell whether the identifier resolved to a user). + */ + private void assertSilentlyAdvances(FlowExecutionResponse response) { + + Assert.assertEquals(response.getFlowStatus(), STATUS_INCOMPLETE); + Assert.assertEquals(response.getType().toString(), TYPE_VIEW); + Assert.assertFalse(hasErrorMessage(response), + "Unexpected ERROR message while the enumeration controls are disabled: " + + response.getData().getMessages()); + Assert.assertTrue(containsOtpComponent(response.getData().getComponents()), + "Expected the OTP verification step to be rendered when the controls are disabled."); + } + + /** + * Whether the response carries any ERROR message (e.g. an enumeration or account-status notification). + */ + private boolean hasErrorMessage(FlowExecutionResponse response) { + + List messages = response.getData().getMessages(); + return messages != null && messages.stream() + .anyMatch(message -> message.getType() == Message.TypeEnum.ERROR); + } + + /** + * Recursively walk the rendered components to detect the OTP verification step, identified by an + * INPUT component with the {@code OTP} variant. + */ + private boolean containsOtpComponent(List components) { + + if (components == null) { + return false; + } + for (Component component : components) { + if ("OTP".equals(component.getVariant())) { + return true; + } + if (containsOtpComponent(component.getComponents())) { + return true; + } + } + return false; + } + + private String createUser(String username, String email) throws Exception { + + UserObject user = new UserObject() + .userName(username) + .password(TEST_USER_PASSWORD) + .name(new Name().givenName(username).familyName("User")) + .addEmail(new Email().value(email)); + return scim2RestClient.createUser(user); + } + + private void setAccountState(String userId, String attribute, boolean value) throws IOException { + + UserItemAddGroupobj patchOp = new UserItemAddGroupobj().op(UserItemAddGroupobj.OpEnum.REPLACE); + patchOp.setPath(USER_SYSTEM_SCHEMA_ATTRIBUTE + ":" + attribute); + patchOp.setValue(value); + scim2RestClient.updateUser(new PatchOperationRequestObject().addOperations(patchOp), userId); + } + + private void updatePasswordRecoveryFeatureStatus(boolean enable) throws IOException { + + ConnectorsPatchReq connectorsPatchReq = new ConnectorsPatchReq(); + connectorsPatchReq.setOperation(ConnectorsPatchReq.OperationEnum.UPDATE); + PropertyReq propertyReq = new PropertyReq(); + propertyReq.setName("Recovery.Notification.Password.emailLink.Enable"); + propertyReq.setValue(enable ? "true" : "false"); + connectorsPatchReq.addProperties(propertyReq); + identityGovernanceRestClient.updateConnectors("QWNjb3VudCBNYW5hZ2VtZW50", + "YWNjb3VudC1yZWNvdmVyeQ", connectorsPatchReq); + } + + private void setAccountDisableHandlerStatus(boolean enable) throws IOException { + + ConnectorsPatchReq connectorsPatchReq = new ConnectorsPatchReq(); + connectorsPatchReq.setOperation(ConnectorsPatchReq.OperationEnum.UPDATE); + PropertyReq propertyReq = new PropertyReq(); + propertyReq.setName("account.disable.handler.enable"); + propertyReq.setValue(enable ? "true" : "false"); + connectorsPatchReq.addProperties(propertyReq); + // Category "Account Management", connector "account.disable.handler". + identityGovernanceRestClient.updateConnectors("QWNjb3VudCBNYW5hZ2VtZW50", + "YWNjb3VudC5kaXNhYmxlLmhhbmRsZXI", connectorsPatchReq); + } + + private void setMultiAttributeLoginStatus(boolean enable) throws IOException { + + ConnectorsPatchReq connectorsPatchReq = new ConnectorsPatchReq(); + connectorsPatchReq.setOperation(ConnectorsPatchReq.OperationEnum.UPDATE); + PropertyReq enableProperty = new PropertyReq(); + enableProperty.setName(MULTI_ATTRIBUTE_ENABLE_PROPERTY); + enableProperty.setValue(enable ? "true" : "false"); + connectorsPatchReq.addProperties(enableProperty); + if (enable) { + PropertyReq allowedAttributesProperty = new PropertyReq(); + allowedAttributesProperty.setName(MULTI_ATTRIBUTE_ALLOWED_ATTRIBUTES_PROPERTY); + allowedAttributesProperty.setValue(MULTI_ATTRIBUTE_ALLOWED_ATTRIBUTES); + connectorsPatchReq.addProperties(allowedAttributesProperty); + } + identityGovernanceRestClient.updateConnectors(ACCOUNT_MGT_CATEGORY, MULTI_ATTRIBUTE_CONNECTOR_ID, + connectorsPatchReq); + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/model/Data.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/model/Data.java index c51c853d278..06df4679453 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/model/Data.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/model/Data.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.Component; +import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.Message; import javax.validation.constraints.*; @@ -46,6 +47,7 @@ public class Data { private Map additionalData = null; + private List messages = null; /** **/ @@ -172,6 +174,32 @@ public Data putAdditionalDataItem(String key, Object additionalDataItem) { return this; } + /** + * Messages surfaced to the user during the flow step. + **/ + public Data messages(List messages) { + + this.messages = messages; + return this; + } + + @ApiModelProperty(value = "") + @JsonProperty("messages") + @Valid + public List getMessages() { + return messages; + } + public void setMessages(List messages) { + this.messages = messages; + } + + public Data addMessagesItem(Message messagesItem) { + if (this.messages == null) { + this.messages = new ArrayList(); + } + this.messages.add(messagesItem); + return this; + } @Override @@ -188,12 +216,13 @@ public boolean equals(java.lang.Object o) { Objects.equals(this.redirectURL, data.redirectURL) && Objects.equals(this.requiredParams, data.requiredParams) && Objects.equals(this.webAuthnData, data.webAuthnData) && - Objects.equals(this.additionalData, data.additionalData); + Objects.equals(this.additionalData, data.additionalData) && + Objects.equals(this.messages, data.messages); } @Override public int hashCode() { - return Objects.hash(components, redirectURL, requiredParams, webAuthnData, additionalData); + return Objects.hash(components, redirectURL, requiredParams, webAuthnData, additionalData, messages); } @Override @@ -207,6 +236,7 @@ public String toString() { sb.append(" requiredParams: ").append(toIndentedString(requiredParams)).append("\n"); sb.append(" webAuthnData: ").append(toIndentedString(webAuthnData)).append("\n"); sb.append(" additionalData: ").append(toIndentedString(additionalData)).append("\n"); + sb.append(" messages: ").append(toIndentedString(messages)).append("\n"); sb.append("}"); return sb.toString(); } diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/model/Message.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/model/Message.java new file mode 100644 index 00000000000..a1a441834bc --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/model/Message.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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.identity.integration.test.rest.api.server.flow.execution.v1.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +import java.util.Objects; + +import javax.validation.Valid; +import javax.xml.bind.annotation.XmlEnum; +import javax.xml.bind.annotation.XmlEnumValue; +import javax.xml.bind.annotation.XmlType; + +/** + * A message surfaced to the user during a flow step (e.g. validation or status messages). + **/ +public class Message { + + @XmlType(name="TypeEnum") + @XmlEnum(String.class) + public enum TypeEnum { + + @XmlEnumValue("ERROR") ERROR(String.valueOf("ERROR")), + @XmlEnumValue("WARNING") WARNING(String.valueOf("WARNING")), + @XmlEnumValue("INFO") INFO(String.valueOf("INFO")); + + private String value; + + TypeEnum(String v) { + value = v; + } + + public String value() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + public static TypeEnum fromValue(String value) { + for (TypeEnum b : TypeEnum.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + } + + private TypeEnum type; + private String message; + private String i18nKey; + + /** + * Message severity type. + **/ + public Message type(TypeEnum type) { + + this.type = type; + return this; + } + + @ApiModelProperty(example = "ERROR", value = "") + @JsonProperty("type") + @Valid + public TypeEnum getType() { + return type; + } + public void setType(TypeEnum type) { + this.type = type; + } + + /** + * Message. + **/ + public Message message(String message) { + + this.message = message; + return this; + } + + @ApiModelProperty(example = "The user does not exist.", value = "") + @JsonProperty("message") + @Valid + public String getMessage() { + return message; + } + public void setMessage(String message) { + this.message = message; + } + + /** + * Internationalization key. + **/ + public Message i18nKey(String i18nKey) { + + this.i18nKey = i18nKey; + return this; + } + + @ApiModelProperty(example = "user.not.found", value = "") + @JsonProperty("i18nKey") + @Valid + public String getI18nKey() { + return i18nKey; + } + public void setI18nKey(String i18nKey) { + this.i18nKey = i18nKey; + } + + @Override + public boolean equals(java.lang.Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Message msg = (Message) o; + return Objects.equals(this.type, msg.type) && + Objects.equals(this.message, msg.message) && + Objects.equals(this.i18nKey, msg.i18nKey); + } + + @Override + public int hashCode() { + return Objects.hash(type, message, i18nKey); + } + + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + sb.append("class Message {\n"); + + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" message: ").append(toIndentedString(message)).append("\n"); + sb.append(" i18nKey: ").append(toIndentedString(i18nKey)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n"); + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/FlowManagementTestBase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/FlowManagementTestBase.java index 5cbf0fb3dd1..f132ad12fd5 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/FlowManagementTestBase.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/FlowManagementTestBase.java @@ -34,6 +34,7 @@ public class FlowManagementTestBase extends RESTAPIServerTestBase { protected static final String REGISTRATION_FLOW = "registration-flow.json"; + protected static final String PASSWORD_RECOVERY_FLOW = "password-recovery-flow.json"; private static final String API_DEFINITION_NAME = "flow.yaml"; protected static final String API_VERSION = "v1"; private static final String API_PACKAGE_NAME = "org.wso2.carbon.identity.api.server.flow.management.v1"; @@ -69,5 +70,6 @@ public void testFinish() { protected static class FlowTypes { public static final String REGISTRATION = "REGISTRATION"; + public static final String PASSWORD_RECOVERY = "PASSWORD_RECOVERY"; } } diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/RecoveryEnumerationControlsManagementNegativeTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/RecoveryEnumerationControlsManagementNegativeTest.java new file mode 100644 index 00000000000..af967b061ec --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/RecoveryEnumerationControlsManagementNegativeTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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.identity.integration.test.rest.api.server.flow.management.v1; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; +import org.wso2.carbon.automation.engine.context.TestUserMode; +import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.FlowConfig; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.Action; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.Component; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.Executor; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.FlowRequest; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.FlowResponse; +import org.wso2.identity.integration.test.restclients.FlowManagementClient; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.wso2.identity.integration.test.rest.api.server.flow.management.v1.FlowManagementTestBase.FlowTypes.PASSWORD_RECOVERY; + +/** + * Negative tests for managing the password recovery flow's user enumeration and account status controls. + */ +public class RecoveryEnumerationControlsManagementNegativeTest extends FlowManagementTestBase { + + private static final String USER_RESOLVE_EXECUTOR = "UserResolveExecutor"; + private static final String NOTIFY_USER_EXISTENCE = "notifyUserExistence"; + private static final String NOTIFY_USER_ACCOUNT_STATUS = "notifyUserAccountStatus"; + private static final String CONTROLS_DISABLED = "false"; + + private FlowManagementClient flowManagementClient; + private static String passwordRecoveryFlowRequestJson; + + @DataProvider(name = "restAPIUserConfigProvider") + public static Object[][] restAPIUserConfigProvider() { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_ADMIN}, + {TestUserMode.TENANT_ADMIN} + }; + } + + @Factory(dataProvider = "restAPIUserConfigProvider") + public RecoveryEnumerationControlsManagementNegativeTest(TestUserMode userMode) throws Exception { + + super.init(userMode); + this.context = isServer; + this.authenticatingUserName = context.getContextTenant().getTenantAdmin().getUserName(); + this.authenticatingCredential = context.getContextTenant().getTenantAdmin().getPassword(); + this.tenant = context.getContextTenant().getDomain(); + } + + @BeforeClass(alwaysRun = true) + public void init() throws Exception { + + super.testInit(API_VERSION, swaggerDefinition, tenantInfo.getDomain()); + flowManagementClient = new FlowManagementClient(serverURL, tenantInfo); + passwordRecoveryFlowRequestJson = readResource(PASSWORD_RECOVERY_FLOW); + updatePasswordRecoveryFlowStatus(true); + } + + @AfterClass(alwaysRun = true) + public void testCleanup() throws Exception { + + updatePasswordRecoveryFlowStatus(false); + flowManagementClient.closeHttpClient(); + super.testConclude(); + } + + @Test(description = "Update the password recovery flow with the enumeration controls disabled on the user " + + "resolve executor.") + public void testUpdateFlowWithControlsDisabled() throws Exception { + + FlowRequest passwordRecoveryFlowRequest = getFlowRequest(); + setNotifyFlags(passwordRecoveryFlowRequest, CONTROLS_DISABLED); + flowManagementClient.putFlow(passwordRecoveryFlowRequest); + } + + @Test(description = "Get the password recovery flow and verify the notify flags round-trip as disabled on the " + + "executor meta.", dependsOnMethods = "testUpdateFlowWithControlsDisabled") + public void testGetFlowReflectsControlsDisabled() throws Exception { + + FlowResponse passwordRecoveryFlowResponse = flowManagementClient.getFlow(PASSWORD_RECOVERY); + Object meta = findUserResolveExecutorMeta( + passwordRecoveryFlowResponse.getSteps().getFirst().getData().getComponents()); + Assert.assertTrue(meta instanceof Map, "UserResolveExecutor meta not found in the retrieved flow."); + + Map metaMap = (Map) meta; + Assert.assertEquals(String.valueOf(metaMap.get(NOTIFY_USER_EXISTENCE)), CONTROLS_DISABLED); + Assert.assertEquals(String.valueOf(metaMap.get(NOTIFY_USER_ACCOUNT_STATUS)), CONTROLS_DISABLED); + } + + private FlowRequest getFlowRequest() throws IOException { + + return new ObjectMapper(new JsonFactory()).readValue(passwordRecoveryFlowRequestJson, FlowRequest.class); + } + + /** + * Set both enumeration-control flags on the {@code UserResolveExecutor} meta to the given value. + */ + @SuppressWarnings("unchecked") + private void setNotifyFlags(FlowRequest flowRequest, String value) { + + Object meta = findUserResolveExecutorMeta(flowRequest.getSteps().get(0).getData().getComponents()); + Assert.assertTrue(meta instanceof Map, "UserResolveExecutor meta not found in the flow definition."); + Map metaMap = (Map) meta; + metaMap.put(NOTIFY_USER_EXISTENCE, value); + metaMap.put(NOTIFY_USER_ACCOUNT_STATUS, value); + } + + private void updatePasswordRecoveryFlowStatus(boolean enable) throws Exception { + + FlowConfig flowConfig = new FlowConfig(); + flowConfig.setFlowType(PASSWORD_RECOVERY); + flowConfig.setIsEnabled(enable); + flowManagementClient.updateFlowConfig(flowConfig); + } + + private Object findUserResolveExecutorMeta(List components) { + + if (components == null) { + return null; + } + for (Component component : components) { + Action action = component.getAction(); + if (action != null && action.getExecutor() != null) { + Executor executor = action.getExecutor(); + if (USER_RESOLVE_EXECUTOR.equals(executor.getName())) { + return executor.getMeta(); + } + } + Object nestedMeta = findUserResolveExecutorMeta(component.getComponents()); + if (nestedMeta != null) { + return nestedMeta; + } + } + return null; + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/RecoveryEnumerationControlsManagementPositiveTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/RecoveryEnumerationControlsManagementPositiveTest.java new file mode 100644 index 00000000000..d26bacac83b --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/RecoveryEnumerationControlsManagementPositiveTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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.identity.integration.test.rest.api.server.flow.management.v1; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; +import org.wso2.carbon.automation.engine.context.TestUserMode; +import org.wso2.identity.integration.test.rest.api.server.flow.execution.v1.model.FlowConfig; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.Action; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.Component; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.Executor; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.FlowRequest; +import org.wso2.identity.integration.test.rest.api.server.flow.management.v1.model.FlowResponse; +import org.wso2.identity.integration.test.restclients.FlowManagementClient; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.wso2.identity.integration.test.rest.api.server.flow.management.v1.FlowManagementTestBase.FlowTypes.PASSWORD_RECOVERY; + +/** + * Positive tests for managing the password recovery flow's user enumeration and account status controls. + */ +public class RecoveryEnumerationControlsManagementPositiveTest extends FlowManagementTestBase { + + private static final String USER_RESOLVE_EXECUTOR = "UserResolveExecutor"; + private static final String NOTIFY_USER_EXISTENCE = "notifyUserExistence"; + private static final String NOTIFY_USER_ACCOUNT_STATUS = "notifyUserAccountStatus"; + + private FlowManagementClient flowManagementClient; + private static String passwordRecoveryFlowRequestJson; + + @DataProvider(name = "restAPIUserConfigProvider") + public static Object[][] restAPIUserConfigProvider() { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_ADMIN}, + {TestUserMode.TENANT_ADMIN} + }; + } + + @Factory(dataProvider = "restAPIUserConfigProvider") + public RecoveryEnumerationControlsManagementPositiveTest(TestUserMode userMode) throws Exception { + + super.init(userMode); + this.context = isServer; + this.authenticatingUserName = context.getContextTenant().getTenantAdmin().getUserName(); + this.authenticatingCredential = context.getContextTenant().getTenantAdmin().getPassword(); + this.tenant = context.getContextTenant().getDomain(); + } + + @BeforeClass(alwaysRun = true) + public void init() throws Exception { + + super.testInit(API_VERSION, swaggerDefinition, tenantInfo.getDomain()); + flowManagementClient = new FlowManagementClient(serverURL, tenantInfo); + passwordRecoveryFlowRequestJson = readResource(PASSWORD_RECOVERY_FLOW); + updatePasswordRecoveryFlowStatus(true); + } + + @AfterClass(alwaysRun = true) + public void testCleanup() throws Exception { + + updatePasswordRecoveryFlowStatus(false); + flowManagementClient.closeHttpClient(); + super.testConclude(); + } + + @Test(description = "Verify the password recovery flow config is enabled.") + public void testPasswordRecoveryFlowConfigEnabled() throws Exception { + + FlowConfig flowConfig = flowManagementClient.getFlowConfig(PASSWORD_RECOVERY); + Assert.assertEquals(flowConfig.getFlowType(), PASSWORD_RECOVERY); + Assert.assertTrue(flowConfig.getIsEnabled()); + } + + @Test(description = "Update the password recovery flow with notify flags set on the user resolve executor.", + dependsOnMethods = "testPasswordRecoveryFlowConfigEnabled") + public void testUpdatePasswordRecoveryFlow() throws Exception { + + FlowRequest passwordRecoveryFlowRequest = getFlowRequest(); + flowManagementClient.putFlow(passwordRecoveryFlowRequest); + } + + @Test(description = "Get the password recovery flow and verify the notify flags round-trip on the executor meta.", + dependsOnMethods = "testUpdatePasswordRecoveryFlow") + public void testGetPasswordRecoveryFlow() throws Exception { + + FlowResponse passwordRecoveryFlowResponse = flowManagementClient.getFlow(PASSWORD_RECOVERY); + Object meta = findUserResolveExecutorMeta( + passwordRecoveryFlowResponse.getSteps().getFirst().getData().getComponents()); + Assert.assertTrue(meta instanceof Map, "UserResolveExecutor meta not found in the retrieved flow."); + + Map metaMap = (Map) meta; + Assert.assertEquals(String.valueOf(metaMap.get(NOTIFY_USER_EXISTENCE)), "true"); + Assert.assertEquals(String.valueOf(metaMap.get(NOTIFY_USER_ACCOUNT_STATUS)), "true"); + } + + private FlowRequest getFlowRequest() throws IOException { + + return new ObjectMapper(new JsonFactory()).readValue(passwordRecoveryFlowRequestJson, FlowRequest.class); + } + + private void updatePasswordRecoveryFlowStatus(boolean enable) throws Exception { + + FlowConfig flowConfig = new FlowConfig(); + flowConfig.setFlowType(PASSWORD_RECOVERY); + flowConfig.setIsEnabled(enable); + flowManagementClient.updateFlowConfig(flowConfig); + } + + private Object findUserResolveExecutorMeta(List components) { + + if (components == null) { + return null; + } + for (Component component : components) { + Action action = component.getAction(); + if (action != null && action.getExecutor() != null) { + Executor executor = action.getExecutor(); + if (USER_RESOLVE_EXECUTOR.equals(executor.getName())) { + return executor.getMeta(); + } + } + Object nestedMeta = findUserResolveExecutorMeta(component.getComponents()); + if (nestedMeta != null) { + return nestedMeta; + } + } + return null; + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/password-recovery-controls-disabled-flow.json b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/password-recovery-controls-disabled-flow.json new file mode 100644 index 00000000000..8dd9a65e976 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/password-recovery-controls-disabled-flow.json @@ -0,0 +1,248 @@ +{ + "steps": [ + { + "id": "view_xx62", + "type": "VIEW", + "size": { + "width": 350, + "height": 556 + }, + "position": { + "x": 300, + "y": 200 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_n6io", + "variant": "H3", + "config": { + "text": "Forgot Password?" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_te8z", + "components": [ + { + "category": "DISPLAY", + "type": "RICH_TEXT", + "config": { + "text": "
We've got you covered. Enter your username to begin resetting your password.
" + }, + "id": "rich_text_83lf" + }, + { + "category": "FIELD", + "type": "INPUT", + "variant": "TEXT", + "config": { + "type": "text", + "hint": "", + "label": "Username", + "required": true, + "placeholder": "Enter your username", + "identifier": "http://wso2.org/claims/username" + }, + "id": "input_i08a" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_1ov4", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "UserResolveExecutor", + "meta": { + "notifyUserExistence": "false", + "notifyUserAccountStatus": "false" + } + }, + "next": "ID_csxv" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "ID_lply", + "type": "VIEW", + "size": { + "width": 350, + "height": 449 + }, + "position": { + "x": 1140, + "y": 319 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_9ci4", + "variant": "H3", + "config": { + "text": "Reset Password" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_4khj", + "components": [ + { + "category": "FIELD", + "type": "INPUT", + "variant": "PASSWORD", + "config": { + "identifier": "password", + "type": "password", + "hint": "", + "label": "Password", + "required": true, + "placeholder": "Enter your password" + }, + "id": "input_x92u" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_x19f", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "PasswordProvisioningExecutor" + }, + "next": "END" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "ID_csxv", + "type": "VIEW", + "size": { + "width": 350, + "height": 449 + }, + "position": { + "x": 735, + "y": 301 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_a99r", + "variant": "H3", + "config": { + "text": "OTP Verification" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_n6zq", + "components": [ + { + "category": "FIELD", + "type": "INPUT", + "variant": "OTP", + "config": { + "type": "text", + "hint": "", + "identifier": "otp", + "label": "Enter the code sent to your email", + "required": false, + "placeholder": "" + }, + "id": "input_nccl" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_yzph", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "EmailOTPExecutor" + }, + "next": "ID_lply" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "END", + "type": "END", + "size": { + "width": 350, + "height": 328 + }, + "position": { + "x": 1555, + "y": 420 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_u2oz", + "variant": "H3", + "config": { + "text": "Password Reset Successfully" + } + }, + { + "category": "DISPLAY", + "type": "RICH_TEXT", + "config": { + "text": "

You have successfully changed your password. You can now sign in with your new password.

" + }, + "id": "display_hgvm" + } + ], + "action": { + "type": "EXECUTOR", + "executor": { + "name": "UserProvisioningExecutor" + } + } + } + } + ], + "flowType": "PASSWORD_RECOVERY" +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/password-recovery-flow.json b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/password-recovery-flow.json new file mode 100644 index 00000000000..271f9ee866e --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/password-recovery-flow.json @@ -0,0 +1,248 @@ +{ + "steps": [ + { + "id": "view_xx62", + "type": "VIEW", + "size": { + "width": 350, + "height": 556 + }, + "position": { + "x": 300, + "y": 200 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_n6io", + "variant": "H3", + "config": { + "text": "Forgot Password?" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_te8z", + "components": [ + { + "category": "DISPLAY", + "type": "RICH_TEXT", + "config": { + "text": "
We've got you covered. Enter your username to begin resetting your password.
" + }, + "id": "rich_text_83lf" + }, + { + "category": "FIELD", + "type": "INPUT", + "variant": "TEXT", + "config": { + "type": "text", + "hint": "", + "label": "Username", + "required": true, + "placeholder": "Enter your username", + "identifier": "http://wso2.org/claims/username" + }, + "id": "input_i08a" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_1ov4", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "UserResolveExecutor", + "meta": { + "notifyUserExistence": "true", + "notifyUserAccountStatus": "true" + } + }, + "next": "ID_csxv" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "ID_lply", + "type": "VIEW", + "size": { + "width": 350, + "height": 449 + }, + "position": { + "x": 1140, + "y": 319 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_9ci4", + "variant": "H3", + "config": { + "text": "Reset Password" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_4khj", + "components": [ + { + "category": "FIELD", + "type": "INPUT", + "variant": "PASSWORD", + "config": { + "identifier": "password", + "type": "password", + "hint": "", + "label": "Password", + "required": true, + "placeholder": "Enter your password" + }, + "id": "input_x92u" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_x19f", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "PasswordProvisioningExecutor" + }, + "next": "END" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "ID_csxv", + "type": "VIEW", + "size": { + "width": 350, + "height": 449 + }, + "position": { + "x": 735, + "y": 301 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_a99r", + "variant": "H3", + "config": { + "text": "OTP Verification" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_n6zq", + "components": [ + { + "category": "FIELD", + "type": "INPUT", + "variant": "OTP", + "config": { + "type": "text", + "hint": "", + "identifier": "otp", + "label": "Enter the code sent to your email", + "required": false, + "placeholder": "" + }, + "id": "input_nccl" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_yzph", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "EmailOTPExecutor" + }, + "next": "ID_lply" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "END", + "type": "END", + "size": { + "width": 350, + "height": 328 + }, + "position": { + "x": 1555, + "y": 420 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_u2oz", + "variant": "H3", + "config": { + "text": "Password Reset Successfully" + } + }, + { + "category": "DISPLAY", + "type": "RICH_TEXT", + "config": { + "text": "

You have successfully changed your password. You can now sign in with your new password.

" + }, + "id": "display_hgvm" + } + ], + "action": { + "type": "EXECUTOR", + "executor": { + "name": "UserProvisioningExecutor" + } + } + } + } + ], + "flowType": "PASSWORD_RECOVERY" +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/password-recovery-multi-attribute-flow.json b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/password-recovery-multi-attribute-flow.json new file mode 100644 index 00000000000..da30d56aba4 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/execution/v1/password-recovery-multi-attribute-flow.json @@ -0,0 +1,248 @@ +{ + "steps": [ + { + "id": "view_xx62", + "type": "VIEW", + "size": { + "width": 350, + "height": 556 + }, + "position": { + "x": 300, + "y": 200 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_n6io", + "variant": "H3", + "config": { + "text": "Forgot Password?" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_te8z", + "components": [ + { + "category": "DISPLAY", + "type": "RICH_TEXT", + "config": { + "text": "
We've got you covered. Enter your username to begin resetting your password.
" + }, + "id": "rich_text_83lf" + }, + { + "category": "FIELD", + "type": "INPUT", + "variant": "TEXT", + "config": { + "type": "text", + "hint": "", + "label": "Username", + "required": true, + "placeholder": "Enter your username", + "identifier": "userIdentifier" + }, + "id": "input_i08a" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_1ov4", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "UserResolveExecutor", + "meta": { + "notifyUserExistence": "true", + "notifyUserAccountStatus": "true" + } + }, + "next": "ID_csxv" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "ID_lply", + "type": "VIEW", + "size": { + "width": 350, + "height": 449 + }, + "position": { + "x": 1140, + "y": 319 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_9ci4", + "variant": "H3", + "config": { + "text": "Reset Password" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_4khj", + "components": [ + { + "category": "FIELD", + "type": "INPUT", + "variant": "PASSWORD", + "config": { + "identifier": "password", + "type": "password", + "hint": "", + "label": "Password", + "required": true, + "placeholder": "Enter your password" + }, + "id": "input_x92u" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_x19f", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "PasswordProvisioningExecutor" + }, + "next": "END" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "ID_csxv", + "type": "VIEW", + "size": { + "width": 350, + "height": 449 + }, + "position": { + "x": 735, + "y": 301 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_a99r", + "variant": "H3", + "config": { + "text": "OTP Verification" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_n6zq", + "components": [ + { + "category": "FIELD", + "type": "INPUT", + "variant": "OTP", + "config": { + "type": "text", + "hint": "", + "identifier": "otp", + "label": "Enter the code sent to your email", + "required": false, + "placeholder": "" + }, + "id": "input_nccl" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_yzph", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "EmailOTPExecutor" + }, + "next": "ID_lply" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "END", + "type": "END", + "size": { + "width": 350, + "height": 328 + }, + "position": { + "x": 1555, + "y": 420 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_u2oz", + "variant": "H3", + "config": { + "text": "Password Reset Successfully" + } + }, + { + "category": "DISPLAY", + "type": "RICH_TEXT", + "config": { + "text": "

You have successfully changed your password. You can now sign in with your new password.

" + }, + "id": "display_hgvm" + } + ], + "action": { + "type": "EXECUTOR", + "executor": { + "name": "UserProvisioningExecutor" + } + } + } + } + ], + "flowType": "PASSWORD_RECOVERY" +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/password-recovery-flow.json b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/password-recovery-flow.json new file mode 100644 index 00000000000..271f9ee866e --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/flow/management/v1/password-recovery-flow.json @@ -0,0 +1,248 @@ +{ + "steps": [ + { + "id": "view_xx62", + "type": "VIEW", + "size": { + "width": 350, + "height": 556 + }, + "position": { + "x": 300, + "y": 200 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_n6io", + "variant": "H3", + "config": { + "text": "Forgot Password?" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_te8z", + "components": [ + { + "category": "DISPLAY", + "type": "RICH_TEXT", + "config": { + "text": "
We've got you covered. Enter your username to begin resetting your password.
" + }, + "id": "rich_text_83lf" + }, + { + "category": "FIELD", + "type": "INPUT", + "variant": "TEXT", + "config": { + "type": "text", + "hint": "", + "label": "Username", + "required": true, + "placeholder": "Enter your username", + "identifier": "http://wso2.org/claims/username" + }, + "id": "input_i08a" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_1ov4", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "UserResolveExecutor", + "meta": { + "notifyUserExistence": "true", + "notifyUserAccountStatus": "true" + } + }, + "next": "ID_csxv" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "ID_lply", + "type": "VIEW", + "size": { + "width": 350, + "height": 449 + }, + "position": { + "x": 1140, + "y": 319 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_9ci4", + "variant": "H3", + "config": { + "text": "Reset Password" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_4khj", + "components": [ + { + "category": "FIELD", + "type": "INPUT", + "variant": "PASSWORD", + "config": { + "identifier": "password", + "type": "password", + "hint": "", + "label": "Password", + "required": true, + "placeholder": "Enter your password" + }, + "id": "input_x92u" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_x19f", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "PasswordProvisioningExecutor" + }, + "next": "END" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "ID_csxv", + "type": "VIEW", + "size": { + "width": 350, + "height": 449 + }, + "position": { + "x": 735, + "y": 301 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_a99r", + "variant": "H3", + "config": { + "text": "OTP Verification" + } + }, + { + "category": "BLOCK", + "type": "FORM", + "config": {}, + "id": "form_n6zq", + "components": [ + { + "category": "FIELD", + "type": "INPUT", + "variant": "OTP", + "config": { + "type": "text", + "hint": "", + "identifier": "otp", + "label": "Enter the code sent to your email", + "required": false, + "placeholder": "" + }, + "id": "input_nccl" + }, + { + "category": "ACTION", + "type": "BUTTON", + "id": "button_yzph", + "variant": "PRIMARY", + "action": { + "type": "EXECUTOR", + "executor": { + "name": "EmailOTPExecutor" + }, + "next": "ID_lply" + }, + "config": { + "type": "submit", + "text": "Continue" + } + } + ] + } + ] + } + }, + { + "id": "END", + "type": "END", + "size": { + "width": 350, + "height": 328 + }, + "position": { + "x": 1555, + "y": 420 + }, + "data": { + "components": [ + { + "category": "DISPLAY", + "type": "TYPOGRAPHY", + "id": "typography_u2oz", + "variant": "H3", + "config": { + "text": "Password Reset Successfully" + } + }, + { + "category": "DISPLAY", + "type": "RICH_TEXT", + "config": { + "text": "

You have successfully changed your password. You can now sign in with your new password.

" + }, + "id": "display_hgvm" + } + ], + "action": { + "type": "EXECUTOR", + "executor": { + "name": "UserProvisioningExecutor" + } + } + } + } + ], + "flowType": "PASSWORD_RECOVERY" +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/testng.xml b/modules/integration/tests-integration/tests-backend/src/test/resources/testng.xml index f58b65e2b4d..afd0ba9f831 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/resources/testng.xml +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/testng.xml @@ -333,7 +333,11 @@ + + + +