diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/ActionExecutionResponseProcessor.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/ActionExecutionResponseProcessor.java index e40a067a28cb..9e6e2bf97465 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/ActionExecutionResponseProcessor.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/ActionExecutionResponseProcessor.java @@ -22,6 +22,7 @@ import org.wso2.carbon.identity.action.execution.model.ActionExecutionStatus; import org.wso2.carbon.identity.action.execution.model.ActionInvocationErrorResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationFailureResponse; +import org.wso2.carbon.identity.action.execution.model.ActionInvocationIncompleteResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationSuccessResponse; import org.wso2.carbon.identity.action.execution.model.ActionType; import org.wso2.carbon.identity.action.execution.model.Error; @@ -29,6 +30,7 @@ import org.wso2.carbon.identity.action.execution.model.Event; import org.wso2.carbon.identity.action.execution.model.FailedStatus; import org.wso2.carbon.identity.action.execution.model.Failure; +import org.wso2.carbon.identity.action.execution.model.Incomplete; import org.wso2.carbon.identity.action.execution.model.Success; import java.util.Map; @@ -47,6 +49,19 @@ ActionExecutionStatus processSuccessResponse(Map eventC ActionInvocationSuccessResponse successResponse) throws ActionExecutionResponseProcessorException; + /** + * This method processes the incomplete response received from the action execution. + * + * @param eventContext The event context. + * @param actionEvent The action event. + * @param incompleteResponse The incomplete response. + * @return The incomplete status. + * @throws ActionExecutionResponseProcessorException If an error occurs while processing the response. + */ + ActionExecutionStatus processIncompleteResponse(Map eventContext, + Event actionEvent, ActionInvocationIncompleteResponse incompleteResponse) throws + ActionExecutionResponseProcessorException; + default ActionExecutionStatus processErrorResponse(Map eventContext, Event actionEvent, ActionInvocationErrorResponse errorResponse) throws diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/ActionExecutorService.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/ActionExecutorService.java index 7ade719e2eea..7c645213f93e 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/ActionExecutorService.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/ActionExecutorService.java @@ -51,4 +51,18 @@ public interface ActionExecutorService { ActionExecutionStatus execute(ActionType actionType, Map eventContext, String tenantDomain) throws ActionExecutionException; + /** + * Resolve the actions by given the action id list and execute them. + * + * @param actionType Action Type. + * @param actionIdList Lis of action Ids of the actions that need to be executed. + * @param eventContext The event context of the corresponding flow. + * @param tenantDomain Tenant domain. + * @return {@link ActionExecutionStatus} The status of the action execution and the response context. + * @throws ActionExecutionException If an error occurs while executing the action. + */ + ActionExecutionStatus execute(ActionType actionType, String[] actionIdList, + Map eventContext, String tenantDomain) + throws ActionExecutionException; + } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/impl/ActionExecutorServiceImpl.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/impl/ActionExecutorServiceImpl.java index d4c1ffc4eee8..e63a04188b30 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/impl/ActionExecutorServiceImpl.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/impl/ActionExecutorServiceImpl.java @@ -36,12 +36,14 @@ import org.wso2.carbon.identity.action.execution.model.ActionExecutionStatus; import org.wso2.carbon.identity.action.execution.model.ActionInvocationErrorResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationFailureResponse; +import org.wso2.carbon.identity.action.execution.model.ActionInvocationIncompleteResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationSuccessResponse; import org.wso2.carbon.identity.action.execution.model.ActionType; import org.wso2.carbon.identity.action.execution.model.AllowedOperation; import org.wso2.carbon.identity.action.execution.model.Error; import org.wso2.carbon.identity.action.execution.model.Failure; +import org.wso2.carbon.identity.action.execution.model.Incomplete; import org.wso2.carbon.identity.action.execution.model.PerformableOperation; import org.wso2.carbon.identity.action.execution.model.Request; import org.wso2.carbon.identity.action.execution.model.Success; @@ -106,6 +108,7 @@ public boolean isExecutionEnabled(ActionType actionType) { * @param tenantDomain Tenant domain. * @return Action execution status. */ + @Override public ActionExecutionStatus execute(ActionType actionType, Map eventContext, String tenantDomain) throws ActionExecutionException { @@ -131,6 +134,7 @@ public ActionExecutionStatus execute(ActionType actionType, Map execute(ActionType actionType, String[] actionIdList, Map eventContext, String tenantDomain) throws ActionExecutionException { @@ -310,6 +314,10 @@ private ActionExecutionStatus processActionResponse(Action action, return processSuccessResponse(action, (ActionInvocationSuccessResponse) actionInvocationResponse.getResponse(), eventContext, actionRequest, actionExecutionResponseProcessor); + } else if (actionInvocationResponse.isIncomplete()) { + return processIncompleteResponse(action, + (ActionInvocationIncompleteResponse) actionInvocationResponse.getResponse(), + eventContext, actionRequest, actionExecutionResponseProcessor); } else if (actionInvocationResponse.isFailure() && actionInvocationResponse.getResponse() != null) { return processFailureResponse(action, (ActionInvocationFailureResponse) actionInvocationResponse .getResponse(), eventContext, actionRequest, actionExecutionResponseProcessor); @@ -333,14 +341,35 @@ private ActionExecutionStatus processSuccessResponse(Action action, logSuccessResponse(action, successResponse); List allowedPerformableOperations = - validatePerformableOperations(actionRequest, successResponse, action); + validatePerformableOperations(actionRequest, successResponse.getOperations(), action); ActionInvocationSuccessResponse.Builder successResponseBuilder = new ActionInvocationSuccessResponse.Builder().actionStatus(ActionInvocationResponse.Status.SUCCESS) - .operations(allowedPerformableOperations); + .operations(allowedPerformableOperations) + .data(successResponse.getData()); return actionExecutionResponseProcessor.processSuccessResponse(eventContext, actionRequest.getEvent(), successResponseBuilder.build()); } + private ActionExecutionStatus processIncompleteResponse( + Action action, + ActionInvocationIncompleteResponse incompleteResponse, + Map eventContext, + ActionExecutionRequest actionRequest, + ActionExecutionResponseProcessor actionExecutionResponseProcessor) + throws ActionExecutionResponseProcessorException { + + //logSuccessResponse(action, successResponse); + + List allowedPerformableOperations = + validatePerformableOperations(actionRequest, incompleteResponse.getOperations(), action); + ActionInvocationIncompleteResponse.Builder incompleteResponseBuilder = + new ActionInvocationIncompleteResponse.Builder() + .actionStatus(ActionInvocationResponse.Status.INCOMPLETE) + .operations(allowedPerformableOperations); + return actionExecutionResponseProcessor.processIncompleteResponse(eventContext, + actionRequest.getEvent(), incompleteResponseBuilder.build()); + } + private ActionExecutionStatus processErrorResponse(Action action, ActionInvocationErrorResponse errorResponse, Map eventContext, @@ -472,11 +501,11 @@ private String serializeFailureResponse(ActionInvocationFailureResponse response } private List validatePerformableOperations( - ActionExecutionRequest request, ActionInvocationSuccessResponse response, Action action) { + ActionExecutionRequest request, List operations, Action action) { List allowedOperations = request.getAllowedOperations(); - List allowedPerformableOperations = response.getOperations().stream() + List allowedPerformableOperations = operations.stream() .filter(performableOperation -> allowedOperations.stream() .anyMatch(allowedOperation -> OperationComparator.compare(allowedOperation, performableOperation))) @@ -486,7 +515,7 @@ private List validatePerformableOperations( List allowedOps = new ArrayList<>(); List notAllowedOps = new ArrayList<>(); - response.getOperations().forEach(operation -> { + operations.forEach(operation -> { String operationDetails = "Operation: " + operation.getOp() + " Path: " + operation.getPath(); if (allowedPerformableOperations.contains(operation)) { allowedOps.add(operationDetails); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionExecutionStatus.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionExecutionStatus.java index dfe00c29a212..336344b88413 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionExecutionStatus.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionExecutionStatus.java @@ -25,7 +25,8 @@ * Action Execution Status is the status object that is returned by the Action Executor Service after executing an * action. It contains the status of the action execution and the response context. * - * @param Status type (i.e. SUCCESS {@link Success}, FAILED {@link Failure}, ERROR {@link Error}) + * @param Status type (i.e. SUCCESS {@link Success}, FAILED {@link Failure}, ERROR {@link Error}, + * INCOMPLETE {@link Incomplete}) */ public abstract class ActionExecutionStatus { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationIncompleteResponse.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationIncompleteResponse.java new file mode 100644 index 000000000000..e69faa54a74d --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationIncompleteResponse.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025, 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.carbon.identity.action.execution.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; + +import java.util.List; + +/** + * This class is used to represent the incomplete response of an action invocation. + * This response will contain the list of operations that need to be performed. + */ +@JsonDeserialize(builder = ActionInvocationIncompleteResponse.Builder.class) +public class ActionInvocationIncompleteResponse implements ActionInvocationResponse.APIResponse { + + private final ActionInvocationResponse.Status actionStatus; + + private final List operations; + + private ActionInvocationIncompleteResponse(Builder builder) { + + this.actionStatus = builder.actionStatus; + this.operations = builder.operations; + } + + @Override + public ActionInvocationResponse.Status getActionStatus() { + + return actionStatus; + } + + public List getOperations() { + + return operations; + } + + /** + * This class is used to build the {@link ActionInvocationIncompleteResponse}. + */ + @JsonPOJOBuilder(withPrefix = "") + public static class Builder { + + private ActionInvocationResponse.Status actionStatus; + private List operations; + + @JsonProperty("actionStatus") + public Builder actionStatus(ActionInvocationResponse.Status actionStatus) { + + this.actionStatus = actionStatus; + return this; + } + + @JsonProperty("operations") + public Builder operations(@JsonProperty("operations") List operations) { + + this.operations = operations; + return this; + } + + public ActionInvocationIncompleteResponse build() { + + if (this.actionStatus == null) { + throw new IllegalArgumentException("actionStatus must not be null."); + } + + if (!ActionInvocationResponse.Status.INCOMPLETE.equals(actionStatus)) { + throw new IllegalArgumentException("actionStatus must be INCOMPLETE."); + } + + if (this.operations == null) { + throw new IllegalArgumentException("operations must not be null."); + } + + return new ActionInvocationIncompleteResponse(this); + } + } +} + diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationResponse.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationResponse.java index abe512e8db43..f7af78736e7d 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationResponse.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationResponse.java @@ -45,6 +45,11 @@ public boolean isSuccess() { return Status.SUCCESS.equals(actionStatus); } + public boolean isIncomplete() { + + return Status.INCOMPLETE.equals(actionStatus); + } + public boolean isFailure() { return Status.FAILED.equals(actionStatus); @@ -70,6 +75,7 @@ public String getErrorLog() { */ public enum Status { SUCCESS, + INCOMPLETE, FAILED, ERROR } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationSuccessResponse.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationSuccessResponse.java index 402580e60f93..3a60118a8133 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationSuccessResponse.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/ActionInvocationSuccessResponse.java @@ -32,13 +32,14 @@ public class ActionInvocationSuccessResponse implements ActionInvocationResponse.APIResponse { private final ActionInvocationResponse.Status actionStatus; - private final List operations; + private final String data; private ActionInvocationSuccessResponse(Builder builder) { this.actionStatus = builder.actionStatus; this.operations = builder.operations; + this.data = builder.data; } @Override @@ -52,6 +53,11 @@ public List getOperations() { return operations; } + public String getData() { + + return data; + } + /** * This class is used to build the {@link ActionInvocationSuccessResponse}. */ @@ -60,6 +66,7 @@ public static class Builder { private ActionInvocationResponse.Status actionStatus; private List operations; + private String data = null; @JsonProperty("actionStatus") public Builder actionStatus(ActionInvocationResponse.Status actionStatus) { @@ -75,6 +82,13 @@ public Builder operations(@JsonProperty("operations") List return this; } + @JsonProperty("data") + public Builder data(@JsonProperty("data") String data) { + + this.data = data; + return this; + } + public ActionInvocationSuccessResponse build() { if (this.actionStatus == null) { diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/Incomplete.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/Incomplete.java new file mode 100644 index 000000000000..20ab3da773b1 --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/Incomplete.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025, 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.carbon.identity.action.execution.model; + +/** + * This interface models the Incomplete status. + * If the downstream extension needs to compose the responses with INCOMPLETE status and communicate it back, + * this class can be used by consumers to implement the model for that incomplete response. + */ +public interface Incomplete { + +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/IncompleteStatus.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/IncompleteStatus.java new file mode 100644 index 000000000000..a3bbbfabf891 --- /dev/null +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/IncompleteStatus.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, 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.carbon.identity.action.execution.model; + +import java.util.Map; + +/** + * This class models the IncompleteStatus. + */ +public class IncompleteStatus extends ActionExecutionStatus { + + private final Incomplete incomplete; + + private IncompleteStatus(Builder builder) { + + this.status = Status.INCOMPLETE; + this.incomplete = builder.incomplete; + this.responseContext = builder.responseContext; + } + + @Override + public Incomplete getResponse() { + + return incomplete; + } + + /** + * This class is the builder for IncompleteStatus. + */ + public static class Builder { + + private Incomplete incomplete; + private Map responseContext; + + public Builder incomplete(Incomplete incomplete) { + + this.incomplete = incomplete; + return this; + } + + public Builder responseContext(Map responseContext) { + + this.responseContext = responseContext; + return this; + } + + public IncompleteStatus build() { + + return new IncompleteStatus(this); + } + } +} diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/PerformableOperation.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/PerformableOperation.java index d07765ccc0b3..5d3f9d2104b1 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/PerformableOperation.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/model/PerformableOperation.java @@ -31,6 +31,7 @@ public class PerformableOperation { private Operation op; private String path; private Object value; + private String url; public Operation getOp() { @@ -49,6 +50,9 @@ public String getPath() { public void setPath(String path) { + if (Operation.REDIRECT.equals(op)) { + throw new IllegalArgumentException("Path is not allowed for REDIRECT operation."); + } this.path = path; } @@ -59,6 +63,22 @@ public Object getValue() { public void setValue(Object value) { + if (Operation.REDIRECT.equals(op)) { + throw new IllegalArgumentException("Value is not allowed for REDIRECT operation."); + } this.value = value; } + + public String getUrl() { + + return url; + } + + public void setUrl(String url) { + + if (!Operation.REDIRECT.equals(op)) { + throw new IllegalArgumentException("Url is only allowed for REDIRECT operation."); + } + this.url = url; + } } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/util/APIClient.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/util/APIClient.java index dfe23a4f069d..91da4b778936 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/util/APIClient.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/main/java/org/wso2/carbon/identity/action/execution/util/APIClient.java @@ -38,6 +38,7 @@ import org.wso2.carbon.identity.action.execution.model.ActionExecutionStatus; import org.wso2.carbon.identity.action.execution.model.ActionInvocationErrorResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationFailureResponse; +import org.wso2.carbon.identity.action.execution.model.ActionInvocationIncompleteResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationSuccessResponse; @@ -249,6 +250,8 @@ private ActionInvocationResponse.APIResponse deserializeSuccessOrFailureResponse } if (actionStatus.equals(ActionExecutionStatus.Status.SUCCESS.name())) { return objectMapper.readValue(jsonResponse, ActionInvocationSuccessResponse.class); + } else if (actionStatus.equals(ActionExecutionStatus.Status.INCOMPLETE.name())) { + return objectMapper.readValue(jsonResponse, ActionInvocationIncompleteResponse.class); } else { return objectMapper.readValue(jsonResponse, ActionInvocationFailureResponse.class); } diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/impl/ActionExecutorServiceImplTest.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/impl/ActionExecutorServiceImplTest.java index f3414560a6e3..d758f501b691 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/impl/ActionExecutorServiceImplTest.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/impl/ActionExecutorServiceImplTest.java @@ -21,12 +21,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang.StringUtils; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; 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.action.execution.ActionExecutionRequestBuilder; import org.wso2.carbon.identity.action.execution.ActionExecutionResponseProcessor; @@ -37,6 +39,7 @@ import org.wso2.carbon.identity.action.execution.model.ActionExecutionStatus; import org.wso2.carbon.identity.action.execution.model.ActionInvocationErrorResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationFailureResponse; +import org.wso2.carbon.identity.action.execution.model.ActionInvocationIncompleteResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationSuccessResponse; import org.wso2.carbon.identity.action.execution.model.ActionType; @@ -48,6 +51,7 @@ import org.wso2.carbon.identity.action.execution.model.FailedStatus; import org.wso2.carbon.identity.action.execution.model.Failure; import org.wso2.carbon.identity.action.execution.model.Header; +import org.wso2.carbon.identity.action.execution.model.IncompleteStatus; import org.wso2.carbon.identity.action.execution.model.Operation; import org.wso2.carbon.identity.action.execution.model.Organization; import org.wso2.carbon.identity.action.execution.model.Param; @@ -327,8 +331,18 @@ public void testActionExecuteWithActionIdsFailureWhenNoRegisteredResponseProcess actionExecutorService.execute(ActionType.PRE_ISSUE_ACCESS_TOKEN, new String[]{any()}, any(), any()); } - @Test - public void testBuildActionExecutionRequestWithExcludedHeaders() throws Exception { + @DataProvider + public Object[][] dataProviderForResponseCreation() { + + return new Object[][]{ + {null}, + {StringUtils.EMPTY}, + {"{\"data\":\"value\"}"} + }; + } + + @Test(dataProvider = "dataProviderForResponseCreation") + public void testBuildActionExecutionRequestWithExcludedHeaders(String dataForResponseCreation) throws Exception { ActionType actionType = ActionType.PRE_ISSUE_ACCESS_TOKEN; Map eventContext = Collections.emptyMap(); @@ -359,7 +373,8 @@ public void testBuildActionExecutionRequestWithExcludedHeaders() throws Exceptio actionExecutionRequest); // Mock APIClient response - ActionInvocationResponse actionInvocationResponse = createSuccessActionInvocationResponse(); + ActionInvocationResponse actionInvocationResponse = + createSuccessActionInvocationResponse(dataForResponseCreation); when(apiClient.callAPI(any(), any(), any())).thenReturn(actionInvocationResponse); // Execute @@ -370,8 +385,8 @@ public void testBuildActionExecutionRequestWithExcludedHeaders() throws Exceptio verify(apiClient).callAPI(any(), any(), eq(payload)); } - @Test - public void testBuildActionExecutionRequest() throws Exception { + @Test(dataProvider = "dataProviderForResponseCreation") + public void testBuildActionExecutionRequest(String dataForResponseCreation) throws Exception { ActionType actionType = ActionType.PRE_ISSUE_ACCESS_TOKEN; Map eventContext = Collections.emptyMap(); @@ -398,7 +413,8 @@ public void testBuildActionExecutionRequest() throws Exception { actionExecutionRequest); // Mock APIClient response - ActionInvocationResponse actionInvocationResponse = createSuccessActionInvocationResponse(); + ActionInvocationResponse actionInvocationResponse = + createSuccessActionInvocationResponse(dataForResponseCreation); when(apiClient.callAPI(any(), any(), any())).thenReturn(actionInvocationResponse); // Execute @@ -409,8 +425,8 @@ public void testBuildActionExecutionRequest() throws Exception { verify(apiClient).callAPI(any(), any(), eq(payload)); } - @Test - public void testExecuteSuccess() throws Exception { + @Test(dataProvider = "dataProviderForResponseCreation") + public void testExecuteSuccess(String dataForResponseCreation) throws Exception { // Setup ActionType actionType = ActionType.PRE_ISSUE_ACCESS_TOKEN; Map eventContext = Collections.emptyMap(); @@ -436,7 +452,8 @@ public void testExecuteSuccess() throws Exception { mock(ActionExecutionRequest.class)); // Mock APIClient response - ActionInvocationResponse actionInvocationResponse = createSuccessActionInvocationResponse(); + ActionInvocationResponse actionInvocationResponse = + createSuccessActionInvocationResponse(dataForResponseCreation); when(apiClient.callAPI(any(), any(), any())).thenReturn(actionInvocationResponse); // Configure response processor @@ -506,6 +523,53 @@ public void testExecuteFailure() throws Exception { assertEquals(actionExecutionStatusWithActionIds.getStatus(), expectedStatus.getStatus()); } + @Test + public void testExecuteIncomplete() throws Exception { + // Setup + ActionType actionType = ActionType.PRE_ISSUE_ACCESS_TOKEN; + Map eventContext = Collections.emptyMap(); + + // Mock Action and its dependencies + Action action = createAction(); + + // Mock ActionManagementService + when(actionManagementService.getActionsByActionType(any(), any())).thenReturn( + Collections.singletonList(action)); + + // Mock ActionRequestBuilder and ActionResponseProcessor + actionExecutionRequestBuilderFactory.when( + () -> ActionExecutionRequestBuilderFactory.getActionExecutionRequestBuilder(any())) + .thenReturn(actionExecutionRequestBuilder); + actionExecutionResponseProcessorFactory.when(() -> ActionExecutionResponseProcessorFactory + .getActionExecutionResponseProcessor(any())) + .thenReturn(actionExecutionResponseProcessor); + + // Configure request builder + when(actionExecutionRequestBuilder.getSupportedActionType()).thenReturn(actionType); + when(actionExecutionRequestBuilder.buildActionExecutionRequest(eventContext)).thenReturn( + mock(ActionExecutionRequest.class)); + + // Mock APIClient response + ActionInvocationResponse actionInvocationResponse = createIncompleteActionInvocationResponse(); + when(apiClient.callAPI(any(), any(), any())).thenReturn(actionInvocationResponse); + + // Configure response processor + ActionExecutionStatus expectedStatus = new IncompleteStatus.Builder().build(); + when(actionExecutionResponseProcessor.getSupportedActionType()).thenReturn(actionType); + when(actionExecutionResponseProcessor.processIncompleteResponse(any(), any(), any())).thenReturn( + expectedStatus); + when(actionManagementService.getActionByActionId(any(), any(), any())).thenReturn(action); + + // Execute and assert + ActionExecutionStatus actualStatus = + actionExecutorService.execute(actionType, eventContext, "tenantDomain"); + assertEquals(actualStatus.getStatus(), expectedStatus.getStatus()); + + ActionExecutionStatus actionExecutionStatusWithActionIds = actionExecutorService.execute( + actionType, new String[]{action.getId()}, eventContext, "tenantDomain"); + assertEquals(actionExecutionStatusWithActionIds.getStatus(), expectedStatus.getStatus()); + } + @Test(expectedExceptions = ActionExecutionException.class, expectedExceptionsMessageRegExp = "Received an invalid or unexpected response for action type: " + "PRE_ISSUE_ACCESS_TOKEN action ID: actionId") @@ -603,11 +667,12 @@ private String getJSONRequestPayload(ActionExecutionRequest actionExecutionReque return requestObjectmapper.writeValueAsString(actionExecutionRequest); } - private ActionInvocationResponse createSuccessActionInvocationResponse() throws Exception { + private ActionInvocationResponse createSuccessActionInvocationResponse(String data) throws Exception { ActionInvocationSuccessResponse successResponse = mock(ActionInvocationSuccessResponse.class); when(successResponse.getActionStatus()).thenReturn(ActionInvocationResponse.Status.SUCCESS); when(successResponse.getOperations()).thenReturn(Collections.emptyList()); + when(successResponse.getData()).thenReturn(data); ActionInvocationResponse actionInvocationResponse = mock(ActionInvocationResponse.class); setField(actionInvocationResponse, "actionStatus", ActionInvocationResponse.Status.SUCCESS); @@ -616,6 +681,19 @@ private ActionInvocationResponse createSuccessActionInvocationResponse() throws return actionInvocationResponse; } + private ActionInvocationResponse createIncompleteActionInvocationResponse() { + + ActionInvocationIncompleteResponse incompleteResponse = mock(ActionInvocationIncompleteResponse.class); + when(incompleteResponse.getActionStatus()).thenReturn(ActionInvocationResponse.Status.INCOMPLETE); + when(incompleteResponse.getOperations()).thenReturn(Collections.emptyList()); + + ActionInvocationResponse actionInvocationResponse = mock(ActionInvocationResponse.class); + when(actionInvocationResponse.isIncomplete()).thenReturn(true); + when(actionInvocationResponse.getResponse()).thenReturn(incompleteResponse); + return actionInvocationResponse; + } + + private ActionInvocationResponse createFailureActionInvocationResponse() { ActionInvocationFailureResponse failureResponse = mock(ActionInvocationFailureResponse.class); diff --git a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/APIClientTest.java b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/APIClientTest.java index dd8b818a641e..075ced3de1a1 100644 --- a/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/APIClientTest.java +++ b/components/action-mgt/org.wso2.carbon.identity.action.execution/src/test/java/org/wso2/carbon/identity/action/execution/util/APIClientTest.java @@ -39,6 +39,7 @@ import org.testng.annotations.Test; import org.wso2.carbon.identity.action.execution.model.ActionInvocationErrorResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationFailureResponse; +import org.wso2.carbon.identity.action.execution.model.ActionInvocationIncompleteResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationResponse; import org.wso2.carbon.identity.action.execution.model.ActionInvocationSuccessResponse; import org.wso2.carbon.identity.action.execution.model.Operation; @@ -152,13 +153,33 @@ public void testCallAPIUnacceptablePayloadForSuccessResponse(String payload) "Unexpected response for status code: 200. Reading JSON response failed."); } - @Test - public void testCallAPIAcceptablePayloadForSuccessResponse() throws Exception { + @DataProvider + public Object[][] acceptableSuccessResponsePayloads() { - String successResponse = + String successResponseWithNoData = "{\"actionStatus\":\"SUCCESS\",\"operations\":[" + "{\"op\":\"add\",\"path\":\"/accessToken/claims/-\",\"" + "value\":{\"name\":\"customSID\",\"value\":\"12345\"}}]}"; + String successResponseWithEmptyData = + "{\"actionStatus\":\"SUCCESS\",\"operations\":[" + + "{\"op\":\"add\",\"path\":\"/accessToken/claims/-\",\"" + + "value\":{\"name\":\"customSID\",\"value\":\"12345\"}}]," + + "\"data\":\"{}\"}"; + String successResponseWithNonEmptyData = + "{\"actionStatus\":\"SUCCESS\",\"operations\":[" + + "{\"op\":\"add\",\"path\":\"/accessToken/claims/-\",\"" + + "value\":{\"name\":\"customSID\",\"value\":\"12345\"}}]," + + "\"data\": {\"id\": \"9f1ab106-ce85-46b1-8f41-6a071b54eb56\"}}"; + + return new Object[][]{ + {successResponseWithNoData, null}, + {successResponseWithEmptyData, "{}"} + }; + } + + @Test(dataProvider = "acceptableSuccessResponsePayloads") + public void testCallAPIAcceptablePayloadForSuccessResponse(String successResponse, + String dataInResponse) throws Exception { when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); when(httpResponse.getStatusLine()).thenReturn(statusLine); @@ -178,6 +199,7 @@ public void testCallAPIAcceptablePayloadForSuccessResponse() throws Exception { ((ActionInvocationSuccessResponse) apiResponse.getResponse()).getOperations().forEach(operation -> { assertEquals(operation.getOp(), Operation.ADD); assertEquals(operation.getPath(), "/accessToken/claims/-"); + assertNull(operation.getUrl()); assertTrue(operation.getValue() instanceof HashMap); ((HashMap) operation.getValue()).forEach((key, value) -> { if ("name".equals(key)) { @@ -187,6 +209,41 @@ public void testCallAPIAcceptablePayloadForSuccessResponse() throws Exception { } }); }); + assertEquals(((ActionInvocationSuccessResponse) apiResponse.getResponse()).getData(), dataInResponse); + assertFalse(apiResponse.isRetry()); + assertNull(apiResponse.getErrorLog()); + } + + @Test + public void testCallAPIAcceptablePayloadForIncompleteResponse() throws Exception { + + String incompleteResponse = + "{\"actionStatus\": \"INCOMPLETE\", \"operations\": [" + + "{\"op\": \"redirect\",\"url\": \"https://dummy-url\"}]}"; + + when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK); + + InputStreamEntity entity = new InputStreamEntity(new ByteArrayInputStream(incompleteResponse.getBytes( + StandardCharsets.UTF_8))); + entity.setContentType(ContentType.APPLICATION_JSON.getMimeType()); + when(httpResponse.getEntity()).thenReturn(entity); + + ActionInvocationResponse apiResponse = apiClient.callAPI("http://example.com", null, "{}"); + + assertNotNull(apiResponse); + assertNotNull(apiResponse.getResponse()); + assertTrue(apiResponse.getResponse() instanceof ActionInvocationIncompleteResponse); + ActionInvocationIncompleteResponse response = + ((ActionInvocationIncompleteResponse) apiResponse.getResponse()); + assertEquals(response.getActionStatus(), ActionInvocationResponse.Status.INCOMPLETE); + response.getOperations().forEach(operation -> { + assertEquals(operation.getOp(), Operation.REDIRECT); + assertEquals(operation.getUrl(), "https://dummy-url"); + assertNull(operation.getPath()); + assertNull(operation.getValue()); + }); assertFalse(apiResponse.isRetry()); assertNull(apiResponse.getErrorLog()); }