From 1337134cd4b3e73ba1c7b8c158176b9fb996b3c4 Mon Sep 17 00:00:00 2001 From: Nidhi Date: Mon, 15 May 2023 13:16:59 +0530 Subject: [PATCH] chore: Refactoring action execution flows to clean up files (#23265) This task is simply dividing action execution related flows into separate files for clarity. It will allow us to pull this module out in the future. --- .../external/models/DatasourceStorage.java | 2 +- .../server/controllers/ActionController.java | 6 +- .../controllers/ce/ActionControllerCE.java | 7 +- .../server/services/NewActionServiceImpl.java | 9 +- .../services/ce/NewActionServiceCE.java | 8 - .../services/ce/NewActionServiceCEImpl.java | 980 --------- .../solutions/ActionExecutionSolution.java | 6 + .../ActionExecutionSolutionImpl.java | 40 + .../ce/ActionExecutionSolutionCE.java | 22 + .../ce/ActionExecutionSolutionCEImpl.java | 1127 ++++++++++ .../services/ce/ActionServiceCE_Test.java | 1605 -------------- .../ce/NewActionServiceCEImplTest.java | 274 --- .../ce/ActionExecutionSolutionCEImplTest.java | 404 ++++ .../ce/ActionExecutionSolutionCETest.java | 1838 +++++++++++++++++ 14 files changed, 3450 insertions(+), 2878 deletions(-) create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ActionExecutionSolution.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ActionExecutionSolutionImpl.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCE.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java create mode 100644 app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImplTest.java create mode 100644 app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCETest.java diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStorage.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStorage.java index e4819b7408..c1f49f3efe 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStorage.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStorage.java @@ -19,7 +19,7 @@ import java.util.Set; @AllArgsConstructor @NoArgsConstructor @Document -public class DatasourceStorage extends BaseDomain{ +public class DatasourceStorage extends BaseDomain { @JsonView(Views.Public.class) String datasourceId; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java index 38d5a74273..c5aa3032aa 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java @@ -4,6 +4,7 @@ import com.appsmith.server.constants.Url; import com.appsmith.server.controllers.ce.ActionControllerCE; import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.NewActionService; +import com.appsmith.server.solutions.ActionExecutionSolution; import com.appsmith.server.solutions.RefactoringSolution; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; @@ -16,9 +17,10 @@ public class ActionController extends ActionControllerCE { public ActionController(LayoutActionService layoutActionService, NewActionService newActionService, - RefactoringSolution refactoringSolution) { + RefactoringSolution refactoringSolution, + ActionExecutionSolution actionExecutionSolution) { - super(layoutActionService, newActionService, refactoringSolution); + super(layoutActionService, newActionService, refactoringSolution, actionExecutionSolution); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ActionControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ActionControllerCE.java index 1f430c5f39..c25de5e29a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ActionControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ActionControllerCE.java @@ -12,6 +12,7 @@ import com.appsmith.server.dtos.RefactorActionNameDTO; import com.appsmith.server.dtos.ResponseDTO; import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.NewActionService; +import com.appsmith.server.solutions.ActionExecutionSolution; import com.appsmith.server.solutions.RefactoringSolution; import com.fasterxml.jackson.annotation.JsonView; @@ -45,14 +46,16 @@ public class ActionControllerCE { private final LayoutActionService layoutActionService; private final NewActionService newActionService; private final RefactoringSolution refactoringSolution; + private final ActionExecutionSolution actionExecutionSolution; @Autowired public ActionControllerCE(LayoutActionService layoutActionService, NewActionService newActionService, - RefactoringSolution refactoringSolution) { + RefactoringSolution refactoringSolution, ActionExecutionSolution actionExecutionSolution) { this.layoutActionService = layoutActionService; this.newActionService = newActionService; this.refactoringSolution = refactoringSolution; + this.actionExecutionSolution = actionExecutionSolution; } @JsonView(Views.Public.class) @@ -82,7 +85,7 @@ public class ActionControllerCE { public Mono> executeAction(@RequestBody Flux partFlux, @RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName, @RequestHeader(name = FieldName.ENVIRONMENT_NAME, required = false) String environmentName) { - return newActionService.executeAction(partFlux, branchName, environmentName) + return actionExecutionSolution.executeAction(partFlux, branchName, environmentName) .map(updatedResource -> new ResponseDTO<>(HttpStatus.OK.value(), updatedResource, null)); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java index 25dc85967e..de1f0249af 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java @@ -30,15 +30,12 @@ public class NewActionServiceImpl extends NewActionServiceCEImpl implements NewA AnalyticsService analyticsService, DatasourceService datasourceService, PluginService pluginService, - DatasourceContextService datasourceContextService, PluginExecutorHelper pluginExecutorHelper, MarketplaceService marketplaceService, PolicyGenerator policyGenerator, NewPageService newPageService, ApplicationService applicationService, - SessionUserService sessionUserService, PolicyUtils policyUtils, - AuthenticationValidator authenticationValidator, ConfigService configService, ResponseUtils responseUtils, PermissionGroupService permissionGroupService, @@ -49,9 +46,9 @@ public class NewActionServiceImpl extends NewActionServiceCEImpl implements NewA ObservationRegistry observationRegistry) { super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService, - datasourceService, pluginService, datasourceContextService, pluginExecutorHelper, marketplaceService, - policyGenerator, newPageService, applicationService, sessionUserService, policyUtils, - authenticationValidator, configService, responseUtils, permissionGroupService, datasourcePermission, + datasourceService, pluginService, pluginExecutorHelper, marketplaceService, + policyGenerator, newPageService, applicationService, policyUtils, + configService, responseUtils, permissionGroupService, datasourcePermission, applicationPermission, pagePermission, actionPermission, observationRegistry); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java index 820963760c..d4c514179d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java @@ -37,14 +37,6 @@ public interface NewActionServiceCE extends CrudService { Mono updateUnpublishedAction(String id, ActionDTO action); - Mono executeAction(ExecuteActionDTO executeActionDTO, String environmentName); - - Mono executeAction(Flux partsFlux, String branchName, String environmentName); - - Mono getValidActionForExecution(ExecuteActionDTO executeActionDTO, String actionId, NewAction newAction); - - T variableSubstitution(T configuration, Map replaceParamsMap); - Mono findByUnpublishedNameAndPageId(String name, String pageId, AclPermission permission); Mono findActionDTObyIdAndViewMode(String id, Boolean viewMode, AclPermission permission); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java index 46b58361ac..1fe7604c58 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java @@ -151,26 +151,16 @@ public class NewActionServiceCEImpl extends BaseService patternList = new ArrayList<>(); private final NewActionRepository repository; private final DatasourceService datasourceService; private final PluginService pluginService; - private final DatasourceContextService datasourceContextService; private final PluginExecutorHelper pluginExecutorHelper; private final MarketplaceService marketplaceService; private final PolicyGenerator policyGenerator; private final NewPageService newPageService; private final ApplicationService applicationService; - private final SessionUserService sessionUserService; private final PolicyUtils policyUtils; - private final ObjectMapper objectMapper; - private final AuthenticationValidator authenticationValidator; private final ConfigService configService; private final ResponseUtils responseUtils; @@ -192,15 +182,12 @@ public class NewActionServiceCEImpl extends BaseService params) { - if (!CollectionUtils.isEmpty(params)) { - for (Param param : params) { - // In case the parameter values turn out to be null, set it to empty string instead to allow - // the execution to go through no matter what. - if (StringUtils.hasLength(param.getKey()) && param.getValue() == null) { - param.setValue(""); - } - } - } - } - /** - * Fetches and caches action with permission. - * - * @param actionId - * @return actionMono - */ - protected Mono getCachedActionForActionExecution(String actionId) { - return repository.findById(actionId, actionPermission.getExecutePermission()) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId))) - .name(ACTION_EXECUTION_CACHED_ACTION) - .tap(Micrometer.observation(observationRegistry)) - .cache(); - } - - /** - * Retrieves and caches validated actionDTO from actionMono. - * - * @param actionMono - * @param executeActionDTO - * @param actionId - * @return actionDTOMono - */ - protected Mono getCachedActionDTOForActionExecution(Mono actionMono, - ExecuteActionDTO executeActionDTO, - String actionId) { - return actionMono - .flatMap(action -> getValidActionForExecution(executeActionDTO, actionId, action)) - .cache(); - } - - /** - * Fetches, validates and caches the datasource from actionDTO - * - * @param actionDTOMono - * @return datasourceMono - */ - protected Mono getCachedDatasourceForActionExecution(Mono actionDTOMono, String environmentName) { - - return actionDTOMono - .flatMap(actionDTO -> datasourceService.getValidDatasourceFromActionMono(actionDTO, - datasourcePermission.getExecutePermission())) - .flatMap(datasource -> { - // For embedded datasource, validate the datasource for each execution - if (datasource.getId() == null) { - return datasourceService.validateDatasource(datasource); - } - - // The external datasource have already been validated. No need to validate again. - return Mono.just(datasource); - }) - .name(ACTION_EXECUTION_CACHED_DATASOURCE) - .tap(Micrometer.observation(observationRegistry)) - .cache(); - } - - /** - * fetches and caches plugin by pluginId after checking datasource for invalids(issues) - * - * @param datasourceMono - * @param actionId - * @return pluginMono if datasource has no issues and plugin is find, else throws error - */ - protected Mono getCachedPluginForActionExecution(Mono datasourceMono, String actionId) { - return datasourceMono - .flatMap(datasource -> { - Set invalids = datasource.getInvalids(); - if (!CollectionUtils.isEmpty(invalids)) { - log.error("Unable to execute actionId: {} because it's datasource is not valid. Cause: {}", - actionId, ArrayUtils.toString(invalids)); - return Mono.error(new AppsmithException(AppsmithError.INVALID_DATASOURCE, - datasource.getName(), - ArrayUtils.toString(invalids))); - } - return pluginService.findById(datasource.getPluginId()); - }) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PLUGIN))) - .name(ACTION_EXECUTION_CACHED_PLUGIN) - .tap(Micrometer.observation(observationRegistry)) - .cache(); - } - - /** - * Fetches and returns editorConfigLabelMap if datasourceId is present - * - * @param datasourceMono - * @return an Empty hashMap if datasource doesn't have id, else configLabelMap from plugin service - */ - protected Mono getEditorConfigLabelMap(Mono datasourceMono) { - - return datasourceMono - .flatMap(datasource -> { - if (!StringUtils.hasLength(datasource.getId())) { - return Mono.just(new HashMap()); - } - - return pluginService.getEditorConfigLabelMap(datasource.getPluginId()); - }) - .name(ACTION_EXECUTION_EDITOR_CONFIG) - .tap(Micrometer.observation(observationRegistry)); - } - - /** - * Passes the payload to pluginExecutor post datasource validation and context retrieval - *

- * This method validates the datasource, retrieves context and subsequently passes the payload to pluginExecutor for - * further execution of the request. - *

- *

In case of failure the method retries to from context

- * - * @param executeActionDTO - * @param actionDTO - * @param datasource - * @param plugin - * @param pluginExecutor - * @param environmentName - * @return actionExecutionResultMono - */ - protected Mono verifyDatasourceAndMakeRequest(ExecuteActionDTO executeActionDTO, - ActionDTO actionDTO, - Datasource datasource, - Plugin plugin, - PluginExecutor pluginExecutor, - String environmentName) { - - DatasourceContextIdentifier dsContextIdentifier = new DatasourceContextIdentifier(); - - Mono executionMono = - datasourceService.getEvaluatedDSAndDsContextKeyWithEnvMap(datasource, environmentName) - .flatMap(tuple3 -> { - Datasource datasource1 = tuple3.getT1(); - DatasourceContextIdentifier datasourceContextIdentifier = tuple3.getT2(); - Map environmentMap = tuple3.getT3(); - - dsContextIdentifier.setDatasourceId(datasourceContextIdentifier.getDatasourceId()); - dsContextIdentifier.setEnvironmentId(datasourceContextIdentifier.getEnvironmentId()); - - return getValidatedDatasourceForActionExecution(datasource1, datasourceContextIdentifier.getEnvironmentId()) - .zipWhen(validatedDatasource -> getDsContextForActionExecution(validatedDatasource, - plugin, - datasourceContextIdentifier, - environmentMap)) - .flatMap(tuple2 -> { - Datasource validatedDatasource = tuple2.getT1(); - DatasourceContext resourceContext = tuple2.getT2(); - // Now that we have the context (connection details), execute the action. - - Instant requestedAt = Instant.now(); - return ((Mono) - pluginExecutor.executeParameterizedWithMetrics(resourceContext.getConnection(), - executeActionDTO, - validatedDatasource.getDatasourceConfiguration(), - actionDTO.getActionConfiguration(), - observationRegistry)) - .map(actionExecutionResult -> { - ActionExecutionRequest actionExecutionRequest = actionExecutionResult.getRequest(); - if (actionExecutionRequest == null) { - actionExecutionRequest = new ActionExecutionRequest(); - } - - actionExecutionRequest.setActionId(executeActionDTO.getActionId()); - actionExecutionRequest.setRequestedAt(requestedAt); - - actionExecutionResult.setRequest(actionExecutionRequest); - return actionExecutionResult; - }); - }); - }); - - return executionMono.onErrorResume(StaleConnectionException.class, error -> { - log.info("Looks like the connection is stale. Retrying with a fresh context."); - return deleteDatasourceContextForRetry(dsContextIdentifier).then(executionMono); - }); - } - - /** - * This is a composite method for fetching authenticated datasource, datasourceContextIdentifier, and environmentMap - * - * @param datasource - * @param environmentName - * @return - */ - protected Mono>> - getValidatedDatasourceWithDsContextKeyAndEnvMap(Datasource datasource, String environmentName) { - // see EE override for complete usage. - return datasourceService.getEvaluatedDSAndDsContextKeyWithEnvMap(datasource, environmentName) - .flatMap(tuple3 -> { - Datasource datasource1 = tuple3.getT1(); - DatasourceContextIdentifier datasourceContextIdentifier = tuple3.getT2(); - Map environmentMap = tuple3.getT3(); - - return getValidatedDatasourceForActionExecution(datasource1, environmentName) - .flatMap(datasource2 -> Mono.zip(Mono.just(datasource2), - Mono.just(datasourceContextIdentifier), - Mono.just(environmentMap)) - ); - }); - } - - /** - * Validates the datasource for further execution - * - * @param datasource - * @return - */ - protected Mono getValidatedDatasourceForActionExecution(Datasource datasource, String environmentId) { - // the environmentName argument is not consumed over here - // See EE override for usage of variable - return authenticationValidator.validateAuthentication(datasource, environmentId) - .name(ACTION_EXECUTION_VALIDATE_AUTHENTICATION) - .tap(Micrometer.observation(observationRegistry)) - .cache(); - } - - /** - * Provides datasource context for execution - * - * @param validatedDatasource - * @param plugin - * @param datasourceContextIdentifier - * @param environmentMap - * @return datasourceContextMono - */ - protected Mono> getDsContextForActionExecution(Datasource validatedDatasource, Plugin plugin, - DatasourceContextIdentifier datasourceContextIdentifier, - Map environmentMap) { - if (plugin.isRemotePlugin()) { - return datasourceContextService.getRemoteDatasourceContext(plugin, validatedDatasource) - .tag("plugin", plugin.getPackageName()) - .name(ACTION_EXECUTION_DATASOURCE_CONTEXT_REMOTE) - .tap(Micrometer.observation(observationRegistry)); - } - return datasourceContextService.getDatasourceContext(validatedDatasource, datasourceContextIdentifier, environmentMap) - .tag("plugin", plugin.getPackageName()) - .name(ACTION_EXECUTION_DATASOURCE_CONTEXT) - .tap(Micrometer.observation(observationRegistry)); - } - - /** - * Deletes the datasourceContext for the given datasource - * - * @param datasourceContextIdentifier - * @return datasourceContextMono - */ - protected Mono> deleteDatasourceContextForRetry(DatasourceContextIdentifier datasourceContextIdentifier) { - // the environmentName argument is not consumed over here - // See EE override for usage of variable - return datasourceContextService.deleteDatasourceContext(datasourceContextIdentifier); - } - - protected Mono handleExecutionErrors(Mono actionExecutionResultMono, - ActionDTO actionDTO, - Integer timeoutDuration, - String actionId) { - return actionExecutionResultMono - .onErrorMap(TimeoutException.class, error -> - new AppsmithPluginException(AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR, - actionDTO.getName(), - timeoutDuration)) - .onErrorMap(StaleConnectionException.class, error -> - new AppsmithPluginException(AppsmithPluginError.STALE_CONNECTION_ERROR)) - .onErrorResume(e -> { - log.debug("{}: In the action execution error mode.", - Thread.currentThread().getName(), e); - ActionExecutionResult result = new ActionExecutionResult(); - result.setErrorInfo(e); - result.setIsExecutionSuccess(false); - final ActionExecutionRequest actionExecutionRequest = new ActionExecutionRequest(); - actionExecutionRequest.setActionId(actionId); - actionExecutionRequest.setRequestedAt(Instant.now()); - result.setRequest(actionExecutionRequest); - return Mono.just(result); - }); - - } - - /** - * Handles the execution logic, call to pluginExecutor with the payload post retrieval and validation of action, datasource, and plugin - * - * @param executeActionDTO - * @param actionMono - * @param actionDTOMono - * @param datasourceMono - * @param pluginMono - * @param pluginExecutorMono - * @param actionName - * @param actionId - * @param environmentName - * @return actionExecutionResultMono - */ - protected Mono getActionExecutionResult(ExecuteActionDTO executeActionDTO, - Mono actionMono, - Mono actionDTOMono, - Mono datasourceMono, - Mono pluginMono, - Mono pluginExecutorMono, - AtomicReference actionName, - String actionId, - String environmentName) { - - Mono> executeActionPublishersCache = - Mono.zip(actionDTOMono, datasourceMono, pluginExecutorMono, pluginMono, actionMono).cache(); - - return executeActionPublishersCache - .flatMap(tuple -> { - final ActionDTO actionDTO = tuple.getT1(); - final Datasource datasource = tuple.getT2(); - final PluginExecutor pluginExecutor = tuple.getT3(); - final Plugin plugin = tuple.getT4(); - final NewAction actionFromDb = tuple.getT5(); - - // Set the action name - actionName.set(actionDTO.getName()); - - log.debug("[{}]Execute Action called in Page {}, for action id : {} action name : {}", - Thread.currentThread().getName(), - actionDTO.getPageId(), actionId, actionDTO.getName()); - - Integer timeoutDuration = actionDTO.getActionConfiguration().getTimeoutInMillisecond(); - - Mono actionExecutionResultMono = - verifyDatasourceAndMakeRequest(executeActionDTO, actionDTO, datasource, - plugin, pluginExecutor, environmentName) - .timeout(Duration.ofMillis(timeoutDuration)); - - return handleExecutionErrors(actionExecutionResultMono, actionDTO, timeoutDuration, actionId) - .elapsed() - // Now send the analytics event for this execution - .flatMap(tuple1 -> { - Long timeElapsed = tuple1.getT1(); - ActionExecutionResult result = tuple1.getT2(); - - log.debug("{}: Action {} with id {} execution time : {} ms", - Thread.currentThread().getName(), - actionName.get(), - actionId, - timeElapsed - ); - - return sendExecuteAnalyticsEvent(actionFromDb, actionDTO, datasource, - executeActionDTO, result, timeElapsed) - .then(Mono.just(result)); - }); - }) - .onErrorResume(AppsmithException.class, error -> { - ActionExecutionResult result = new ActionExecutionResult(); - result.setIsExecutionSuccess(false); - result.setErrorInfo(error); - return Mono.just(result); - }); - } - - /** - * Fetches the required Mono (action, datasource, and plugin) and makes actionExecution call to plugin - * - * @param executeActionDTO - * @param environmentName - * @return actionExecutionResult if query succeeds, error messages otherwise - */ - public Mono executeAction(ExecuteActionDTO executeActionDTO, String environmentName) { - - // 1. Validate input parameters which are required for mustache replacements - replaceNullWithQuotesForParamValues(executeActionDTO.getParams()); - - String actionId = executeActionDTO.getActionId(); - AtomicReference actionName = new AtomicReference<>(); - actionName.set(""); - - // 2. Fetch the action from the DB and check if it can be executed - Mono actionMono = getCachedActionForActionExecution(actionId); - Mono actionDTOMono = getCachedActionDTOForActionExecution(actionMono, executeActionDTO, actionId); - - // 3. Instantiate the implementation class based on the query type - Mono datasourceMono = getCachedDatasourceForActionExecution(actionDTOMono, environmentName); - Mono pluginMono = getCachedPluginForActionExecution(datasourceMono, actionId); - Mono pluginExecutorMono = pluginExecutorHelper.getPluginExecutor(pluginMono); - - // 4. Execute the query - Mono actionExecutionResultMono = getActionExecutionResult(executeActionDTO, - actionMono, - actionDTOMono, - datasourceMono, - pluginMono, - pluginExecutorMono, - actionName, - actionId, - environmentName); - - Mono editorConfigLabelMapMono = getEditorConfigLabelMap(datasourceMono); - - return actionExecutionResultMono - .zipWith(editorConfigLabelMapMono, (result, labelMap) -> { - if (TRUE.equals(executeActionDTO.getViewMode())) { - result.setRequest(null); - } else if (result.getRequest() != null && result.getRequest().getRequestParams() != null) { - transformRequestParams(result, labelMap); - } - return result; - }) - .map(result -> addDataTypesAndSetSuggestedWidget(result, executeActionDTO.getViewMode())); - } - - /** - * Creates the ExecuteActionDTO from Flux of ByteBuffers - * - * @param partFlux - * @return an executionDTO object with parameterMap - */ - protected Mono createExecuteActionDTO(Flux partFlux) { - final AtomicLong totalReadableByteCount = new AtomicLong(0); - final ExecuteActionDTO dto = new ExecuteActionDTO(); - return this.parsePartsAndGetParamsFlux(partFlux, totalReadableByteCount, dto) - .collectList() - .flatMap(params -> this.enrichExecutionParam(totalReadableByteCount, dto, params)) - .name(ACTION_EXECUTION_REQUEST_PARSING) - .tap(Micrometer.observation(observationRegistry)); - } - - /** - * Executes the action(queries) by creating executeActionDTO and sending it to the plugin for further execution - * - * @param partFlux - * @param branchName - * @param environmentName - * @return Mono of actionExecutionResult if the query succeeds, error messages otherwise - */ - @Override - public Mono executeAction(Flux partFlux, String branchName, String environmentName) { - return createExecuteActionDTO(partFlux) - .flatMap(executeActionDTO -> findByBranchNameAndDefaultActionId(branchName, - executeActionDTO.getActionId(), - actionPermission.getExecutePermission()) - .map(branchedAction -> { - executeActionDTO.setActionId(branchedAction.getId()); - return executeActionDTO; - })) - .flatMap(executeActionDTO -> this.executeAction(executeActionDTO, environmentName)) - .name(ACTION_EXECUTION_SERVER_EXECUTION) - .tap(Micrometer.observation(observationRegistry)); - } - - - @Override - public Mono getValidActionForExecution(ExecuteActionDTO executeActionDTO, String actionId, NewAction newAction) { - Mono actionDTOMono = Mono.just(newAction) - .flatMap(dbAction -> { - ActionDTO action; - if (TRUE.equals(executeActionDTO.getViewMode())) { - action = dbAction.getPublishedAction(); - // If the action has not been published, return error - if (action == null) { - return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId)); - } - } else { - action = dbAction.getUnpublishedAction(); - } - - // Now check for erroneous situations which would deter the execution of the action : - - // Error out with in case of an invalid action - if (FALSE.equals(action.getIsValid())) { - return Mono.error(new AppsmithException( - AppsmithError.INVALID_ACTION, - action.getName(), - ArrayUtils.toString(action.getInvalids().toArray()) - )); - } - - // Error out in case of JS Plugin (this is currently client side execution only) - if (dbAction.getPluginType() == PluginType.JS) { - return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); - } - return Mono.just(action); - }); - return actionDTOMono; - } - - /* - * - Get label for request params. - * - Transform request params list: [""] to a map: {"label": {"value": ...}} - * - Rearrange request params in the order as they appear in query editor form. - */ - private void transformRequestParams(ActionExecutionResult result, Map labelMap) { - Map transformedParams = new LinkedHashMap<>(); - Map requestParamsConfigMap = new HashMap(); - ((List) result.getRequest().getRequestParams()).stream() - .forEach(param -> requestParamsConfigMap.put(((RequestParamDTO) param).getConfigProperty(), - (RequestParamDTO) param)); - - labelMap.entrySet().stream() - .forEach(e -> { - String configProperty = e.getKey(); - if (requestParamsConfigMap.containsKey(configProperty)) { - RequestParamDTO param = requestParamsConfigMap.get(configProperty); - transformedParams.put(e.getValue(), param); - } - }); - - result.getRequest().setRequestParams(transformedParams); - } - - private ActionExecutionResult addDataTypesAndSetSuggestedWidget(ActionExecutionResult result, Boolean viewMode) { - - if (FALSE.equals(viewMode)) { - result.setSuggestedWidgets(getSuggestedWidgets(result.getBody())); - } - - /* - * - Do not process if data types are already present. - * - It means that data types have been added by specific plugin. - */ - - if (!CollectionUtils.isEmpty(result.getDataTypes())) { - return result; - } - - result.setDataTypes(getDisplayDataTypes(result.getBody())); - - return result; - } - - /** - * Since we're loading the application and other details from DB *only* for analytics, we check if analytics is - * active before making the call to DB. - * - * @return - */ - public Boolean isSendExecuteAnalyticsEvent() { - return analyticsService.isActive(); - } - - private Mono sendExecuteAnalyticsEvent( - NewAction action, - ActionDTO actionDTO, - Datasource datasource, - ExecuteActionDTO executeActionDto, - ActionExecutionResult actionExecutionResult, - Long timeElapsed - ) { - - if (!isSendExecuteAnalyticsEvent()) { - return Mono.empty(); - } - ActionExecutionRequest actionExecutionRequest = actionExecutionResult.getRequest(); - ActionExecutionRequest request; - if (actionExecutionRequest != null) { - // Do a deep copy of request to not edit - request = new ActionExecutionRequest( - actionExecutionRequest.getActionId(), - actionExecutionRequest.getRequestedAt(), - actionExecutionRequest.getQuery(), - actionExecutionRequest.getBody(), - actionExecutionRequest.getHeaders(), - actionExecutionRequest.getHttpMethod(), - actionExecutionRequest.getUrl(), - actionExecutionRequest.getProperties(), - actionExecutionRequest.getExecutionParameters(), - null - ); - } else { - request = new ActionExecutionRequest(); - } - - if (request.getHeaders() != null) { - JsonNode headers = objectMapper.convertValue(request.getHeaders(), JsonNode.class); - try { - final String headersAsString = objectMapper.writeValueAsString(headers); - request.setHeaders(headersAsString); - } catch (JsonProcessingException e) { - log.error(e.getMessage()); - } - } - - if (request.getBody() != null) { - try { - final String bodyAsString = objectMapper.writeValueAsString(request.getBody()); - request.setBody(bodyAsString); - } catch (JsonProcessingException e) { - log.error(e.getMessage()); - request.setBody("\"Error serializing value to JSON.\""); - } - } - - if (!CollectionUtils.isEmpty(request.getProperties())) { - final Map stringProperties = new HashMap<>(); - for (final Map.Entry entry : request.getProperties().entrySet()) { - String jsonValue; - try { - jsonValue = objectMapper.writeValueAsString(entry.getValue()); - } catch (JsonProcessingException e) { - jsonValue = "\"Error serializing value to JSON.\""; - } - stringProperties.put(entry.getKey(), jsonValue); - } - request.setProperties(stringProperties); - } - - return Mono.justOrEmpty(action.getApplicationId()) - .flatMap(applicationService::findById) - .defaultIfEmpty(new Application()) - .flatMap(application -> Mono.zip( - Mono.just(application), - sessionUserService.getCurrentUser(), - newPageService.getNameByPageId(actionDTO.getPageId(), executeActionDto.getViewMode()), - pluginService.getById(action.getPluginId()) - )) - .flatMap(tuple -> { - final Application application = tuple.getT1(); - final User user = tuple.getT2(); - final String pageName = tuple.getT3(); - final Plugin plugin = tuple.getT4(); - - final PluginType pluginType = action.getPluginType(); - final String appMode = TRUE.equals(executeActionDto.getViewMode()) ? ApplicationMode.PUBLISHED.toString() : ApplicationMode.EDIT.toString(); - - final Map data = new HashMap<>(Map.of( - "username", user.getUsername(), - "type", pluginType, - "pluginName", plugin.getName(), - "name", actionDTO.getName(), - "datasource", Map.of( - "name", datasource.getName() - ), - "orgId", application.getWorkspaceId(), - "appId", action.getApplicationId(), - FieldName.APP_MODE, appMode, - "appName", application.getName(), - "isExampleApp", application.isAppIsExample() - )); - - String dsCreatedAt = ""; - if (datasource.getCreatedAt() != null) { - dsCreatedAt = DateUtils.ISO_FORMATTER.format(datasource.getCreatedAt()); - } - List paramsList = executeActionDto.getParams(); - if (paramsList == null) { - paramsList = new ArrayList<>(); - } - List executionParams = paramsList.stream().map(param -> param.getValue()).collect(Collectors.toList()); - - data.putAll(Map.of( - "request", request, - "pageId", ObjectUtils.defaultIfNull(actionDTO.getPageId(), ""), - "pageName", pageName, - "isSuccessfulExecution", ObjectUtils.defaultIfNull(actionExecutionResult.getIsExecutionSuccess(), false), - "statusCode", ObjectUtils.defaultIfNull(actionExecutionResult.getStatusCode(), ""), - "timeElapsed", timeElapsed, - "actionCreated", DateUtils.ISO_FORMATTER.format(action.getCreatedAt()), - "actionId", ObjectUtils.defaultIfNull(action.getId(), "") - )); - data.putAll(Map.of( - FieldName.ACTION_EXECUTION_REQUEST_PARAMS_SIZE, executeActionDto.getTotalReadableByteCount(), - FieldName.ACTION_EXECUTION_REQUEST_PARAMS_COUNT, executionParams.size() - )); - - data.putAll( - Map.of( - "pluginErrorDetails", ObjectUtils.defaultIfNull(actionExecutionResult.getPluginErrorDetails(), "") - ) - ); - data.putAll(Map.of( - "dsId", ObjectUtils.defaultIfNull(datasource.getId(), ""), - "dsName", datasource.getName(), - "dsIsTemplate", ObjectUtils.defaultIfNull(datasource.getIsTemplate(), ""), - "dsIsMock", ObjectUtils.defaultIfNull(datasource.getIsMock(), ""), - "dsCreatedAt", dsCreatedAt - )); - - // Add the error message in case of erroneous execution - if (FALSE.equals(actionExecutionResult.getIsExecutionSuccess())) { - String errorJson; - try { - errorJson = objectMapper.writeValueAsString(actionExecutionResult.getBody()); - } catch (JsonProcessingException e) { - log.warn("Unable to serialize action execution error result to JSON.", e); - errorJson = "\"Failed to serialize error data to JSON.\""; - } - data.put("error", errorJson); - } - - if (actionExecutionResult.getStatusCode() != null) { - data.putAll(Map.of( - "statusCode", actionExecutionResult.getStatusCode() - )); - } - - String executionRequestQuery = ""; - if (actionExecutionResult != null && - actionExecutionResult.getRequest() != null && - actionExecutionResult.getRequest().getQuery() != null) { - executionRequestQuery = actionExecutionResult.getRequest().getQuery(); - } - - final Map eventData = new HashMap<>(Map.of( - FieldName.ACTION, action, - FieldName.DATASOURCE, datasource, - FieldName.APP_MODE, appMode, - FieldName.ACTION_EXECUTION_RESULT, actionExecutionResult, - FieldName.ACTION_EXECUTION_TIME, timeElapsed, - FieldName.ACTION_EXECUTION_QUERY, executionRequestQuery, - FieldName.APPLICATION, application, - FieldName.PLUGIN, plugin - )); - - if (executeActionDto.getTotalReadableByteCount() <= Constraint.MAX_ANALYTICS_SIZE_BYTES) { - // Only send params info if total size is less than 5 MB - eventData.put(FieldName.ACTION_EXECUTION_REQUEST_PARAMS, executionParams); - } else { - eventData.put(FieldName.ACTION_EXECUTION_REQUEST_PARAMS, REDACTED_DATA); - } - data.put(FieldName.EVENT_DATA, eventData); - - return analyticsService.sendObjectEvent(AnalyticsEvents.EXECUTE_ACTION, action, data) - .thenReturn(request); - }) - .onErrorResume(error -> { - log.warn("Error sending action execution data point", error); - return Mono.just(request); - }); - } - - /** - * This function replaces the variables in the Object with the actual params - */ - @Override - public T variableSubstitution(T configuration, Map replaceParamsMap) { - return MustacheHelper.renderFieldValues(configuration, replaceParamsMap); - } @Override public Mono findByUnpublishedNameAndPageId(String name, String pageId, AclPermission permission) { @@ -2340,222 +1575,7 @@ public class NewActionServiceCEImpl extends BaseService parsePartsAndGetParamsFlux(Flux partFlux, AtomicLong totalReadableByteCount, ExecuteActionDTO dto) { - return partFlux - .groupBy(part -> { - // We're grouping parts by the type of processing required - // Expected types: meta, value, blob - for (Pattern pattern : patternList) { - Matcher matcher = pattern.matcher(part.name()); - if (matcher.find()) { - return pattern.pattern(); - } - } - return part.name(); - }) - .flatMap(groupedPartsFlux -> { - String key = groupedPartsFlux.key(); - return switch (key) { - case PARAM_KEY_REGEX -> - groupedPartsFlux.flatMap(part -> this.parseExecuteParameter(part, totalReadableByteCount)); - case BLOB_KEY_REGEX -> - this.parseExecuteBlobs(groupedPartsFlux, dto, totalReadableByteCount).then(Mono.empty()); - case EXECUTE_ACTION_DTO -> - groupedPartsFlux.next().flatMap(part -> this.parseExecuteActionPart(part, dto)).then(Mono.empty()); - case PARAMETER_MAP -> - groupedPartsFlux.next().flatMap(part -> this.parseExecuteParameterMapPart(part, dto)).then(Mono.empty()); - default -> - Mono.error(new AppsmithException(AppsmithError.GENERIC_BAD_REQUEST, "Unexpected part found: " + key)); - }; - }); - } - protected Mono enrichExecutionParam(AtomicLong totalReadableByteCount, ExecuteActionDTO dto, List params) { - if (dto.getActionId() == null) { - return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ACTION_ID)); - } - - dto.setTotalReadableByteCount(totalReadableByteCount.longValue()); - - final Set visitedBindings = new HashSet<>(); - /* - Parts in multipart request can appear in any order. In order to avoid NPE original name of the parameters - along with the client-side data type are set here as it's guaranteed at this point that the part having the parameterMap is already collected. - Ref: https://github.com/appsmithorg/appsmith/issues/16722 - */ - params.forEach( - param -> { - String pseudoBindingName = param.getPseudoBindingName(); - String bindingValue = dto.getInvertParameterMap().get(pseudoBindingName); - param.setKey(bindingValue); - visitedBindings.add(bindingValue); - // if the type is not an array e.g. "k1": "string" or "k1": "boolean" - ParamProperty paramProperty = dto.getParamProperties().get(pseudoBindingName); - if (paramProperty != null) { - this.identifyExecutionParamDatatype(param, paramProperty); - - this.substituteBlobValuesInParam(dto, param, paramProperty); - } - - } - ); - - // In case there are parameters that did not receive a value in the multipart request, - // initialize these bindings with empty strings - if (dto.getParameterMap() != null) { - dto.getParameterMap() - .keySet() - .stream() - .forEach(parameter -> { - if (!visitedBindings.contains(parameter)) { - Param newParam = new Param(parameter, ""); - params.add(newParam); - } - }); - } - dto.setParams(params); - return Mono.just(dto); - } - - private void substituteBlobValuesInParam(ExecuteActionDTO dto, Param param, ParamProperty paramProperty) { - // Check if this param has blobUrlPaths - if (paramProperty.getBlobIdentifiers() != null && !paramProperty.getBlobIdentifiers().isEmpty()) { - // If it does, trigger the replacement logic for each of these urlPaths - String replacedValue = this.replaceBlobValuesInParam( - param.getValue(), - paramProperty.getBlobIdentifiers(), - dto.getBlobValuesMap()); - // And then update the value for this param - param.setValue(replacedValue); - } - } - - private void identifyExecutionParamDatatype(Param param, ParamProperty paramProperty) { - Object datatype = paramProperty.getDatatype(); - if (datatype instanceof String) { - param.setClientDataType(ClientDataType.valueOf(String.valueOf(datatype).toUpperCase())); - } else if (datatype instanceof LinkedHashMap) { - // if the type is an array e.g. "k1": { "array": [ "string", "number", "string", "boolean"] - LinkedHashMap stringArrayListLinkedHashMap = - (LinkedHashMap) datatype; - Optional firstKeyOpt = stringArrayListLinkedHashMap.keySet() - .stream() - .findFirst(); - if (firstKeyOpt.isPresent()) { - String firstKey = firstKeyOpt.get(); - param.setClientDataType(ClientDataType.valueOf(firstKey.toUpperCase())); - List individualTypes = stringArrayListLinkedHashMap.get(firstKey); - List dataTypesOfArrayElements = - individualTypes.stream() - .map(it -> ClientDataType.valueOf(String.valueOf(it) - .toUpperCase())) - .collect(Collectors.toList()); - param.setDataTypesOfArrayElements(dataTypesOfArrayElements); - } - } - } - - protected Mono parseExecuteActionPart(Part part, ExecuteActionDTO dto) { - return DataBufferUtils - .join(part.content()) - .flatMap(executeActionDTOBuffer -> { - byte[] byteData = new byte[executeActionDTOBuffer.readableByteCount()]; - executeActionDTOBuffer.read(byteData); - DataBufferUtils.release(executeActionDTOBuffer); - try { - return Mono.just(objectMapper.readValue(byteData, ExecuteActionDTO.class)); - } catch (IOException e) { - log.error("Error in deserializing ExecuteActionDTO", e); - return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, EXECUTE_ACTION_DTO)); - } - }) - .flatMap(executeActionDTO -> { - dto.setActionId(executeActionDTO.getActionId()); - dto.setViewMode(executeActionDTO.getViewMode()); - dto.setParamProperties(executeActionDTO.getParamProperties()); - dto.setPaginationField(executeActionDTO.getPaginationField()); - return Mono.empty(); - }); - } - - protected Mono parseExecuteParameterMapPart(Part part, ExecuteActionDTO dto) { - return DataBufferUtils - .join(part.content()) - .flatMap(parameterMapBuffer -> { - byte[] byteData = new byte[parameterMapBuffer.readableByteCount()]; - parameterMapBuffer.read(byteData); - DataBufferUtils.release(parameterMapBuffer); - try { - return Mono.just(objectMapper.readValue(byteData, new TypeReference>() { - })); - } catch (IOException e) { - return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, PARAMETER_MAP)); - } - }) - .flatMap(paramMap -> { - dto.setParameterMap(paramMap); - return Mono.empty(); - }); - } - - protected Mono parseExecuteParameter(Part part, AtomicLong totalReadableByteCount) { - final Param param = new Param(); - param.setPseudoBindingName(part.name()); - return DataBufferUtils - .join(part.content()) - .map(dataBuffer -> { - byte[] bytes = new byte[dataBuffer.readableByteCount()]; - totalReadableByteCount.addAndGet(dataBuffer.readableByteCount()); - dataBuffer.read(bytes); - DataBufferUtils.release(dataBuffer); - param.setValue(new String(bytes, StandardCharsets.UTF_8)); - return param; - }); - } - - protected Mono parseExecuteBlobs(Flux partsFlux, ExecuteActionDTO dto, AtomicLong totalReadableByteCount) { - Map blobMap = new HashMap<>(); - dto.setBlobValuesMap(blobMap); - - return partsFlux - .flatMap(part -> { - return DataBufferUtils - .join(part.content()) - .map(dataBuffer -> { - byte[] bytes = new byte[dataBuffer.readableByteCount()]; - totalReadableByteCount.addAndGet(dataBuffer.readableByteCount()); - dataBuffer.read(bytes); - DataBufferUtils.release(dataBuffer); - blobMap.put(part.name(), new String(bytes, StandardCharsets.ISO_8859_1)); - return Mono.empty(); - }); - }) - .then(); - } - - protected String replaceBlobValuesInParam(String value, List blobIdentifiers, Map blobValuesMap) { - // If there is no blobId reference against this param, return as is - if (blobIdentifiers == null || blobIdentifiers.isEmpty()) { - return value; - } - - // Otherwise, for each such blobId reference, replace the reference with the actual value from the blobMap - for (String blobId : blobIdentifiers) { - value = value.replace(blobId, StringEscapeUtils.escapeJava(blobValuesMap.get(blobId))); - } - - return value; - } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ActionExecutionSolution.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ActionExecutionSolution.java new file mode 100644 index 0000000000..5dc0d4bf1a --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ActionExecutionSolution.java @@ -0,0 +1,6 @@ +package com.appsmith.server.solutions; + +import com.appsmith.server.solutions.ce.ActionExecutionSolutionCE; + +public interface ActionExecutionSolution extends ActionExecutionSolutionCE { +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ActionExecutionSolutionImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ActionExecutionSolutionImpl.java new file mode 100644 index 0000000000..74396f906c --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ActionExecutionSolutionImpl.java @@ -0,0 +1,40 @@ +package com.appsmith.server.solutions; + +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.repositories.NewActionRepository; +import com.appsmith.server.services.AnalyticsService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.AuthenticationValidator; +import com.appsmith.server.services.DatasourceContextService; +import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.NewActionService; +import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.PluginService; +import com.appsmith.server.services.SessionUserService; +import com.appsmith.server.solutions.ce.ActionExecutionSolutionCEImpl; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.observation.ObservationRegistry; +import org.springframework.stereotype.Service; + +@Service +public class ActionExecutionSolutionImpl extends ActionExecutionSolutionCEImpl implements ActionExecutionSolution { + public ActionExecutionSolutionImpl(NewActionService newActionService, + ActionPermission actionPermission, + ObservationRegistry observationRegistry, + ObjectMapper objectMapper, + NewActionRepository repository, + DatasourceService datasourceService, + PluginService pluginService, + DatasourceContextService datasourceContextService, + PluginExecutorHelper pluginExecutorHelper, + NewPageService newPageService, + ApplicationService applicationService, + SessionUserService sessionUserService, + AuthenticationValidator authenticationValidator, + DatasourcePermission datasourcePermission, + AnalyticsService analyticsService) { + super(newActionService, actionPermission, observationRegistry, objectMapper, repository, datasourceService, + pluginService, datasourceContextService, pluginExecutorHelper, newPageService, applicationService, + sessionUserService, authenticationValidator, datasourcePermission, analyticsService); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCE.java new file mode 100644 index 0000000000..0e6bc99bbc --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCE.java @@ -0,0 +1,22 @@ +package com.appsmith.server.solutions.ce; + +import com.appsmith.external.dtos.ExecuteActionDTO; +import com.appsmith.external.models.ActionDTO; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.server.domains.NewAction; +import org.springframework.http.codec.multipart.Part; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Map; + +public interface ActionExecutionSolutionCE { + Mono executeAction(Flux partFlux, String branchName, String environmentName); + + Mono executeAction(ExecuteActionDTO executeActionDTO, String environmentName); + + Mono getValidActionForExecution(ExecuteActionDTO executeActionDTO, String actionId, NewAction newAction); + + T variableSubstitution(T configuration, Map replaceParamsMap); + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java new file mode 100644 index 0000000000..c47ffec66b --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImpl.java @@ -0,0 +1,1127 @@ +package com.appsmith.server.solutions.ce; + +import com.appsmith.external.constants.AnalyticsEvents; +import com.appsmith.external.datatypes.ClientDataType; +import com.appsmith.external.dtos.ExecuteActionDTO; +import com.appsmith.external.dtos.ParamProperty; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; +import com.appsmith.external.helpers.MustacheHelper; +import com.appsmith.external.models.ActionDTO; +import com.appsmith.external.models.ActionExecutionRequest; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.BaseDomain; +import com.appsmith.external.models.Datasource; +import com.appsmith.external.models.Param; +import com.appsmith.external.models.PluginType; +import com.appsmith.external.models.RequestParamDTO; +import com.appsmith.external.plugins.PluginExecutor; +import com.appsmith.server.constants.Constraint; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationMode; +import com.appsmith.server.domains.DatasourceContext; +import com.appsmith.server.domains.DatasourceContextIdentifier; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.Plugin; +import com.appsmith.server.domains.User; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.DateUtils; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.repositories.NewActionRepository; +import com.appsmith.server.services.AnalyticsService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.AuthenticationValidator; +import com.appsmith.server.services.DatasourceContextService; +import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.NewActionService; +import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.PluginService; +import com.appsmith.server.services.SessionUserService; +import com.appsmith.server.solutions.ActionPermission; +import com.appsmith.server.solutions.DatasourcePermission; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.observation.ObservationRegistry; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.codec.multipart.Part; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import reactor.core.observability.micrometer.Micrometer; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple3; +import reactor.util.function.Tuple5; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static com.appsmith.external.constants.CommonFieldName.REDACTED_DATA; +import static com.appsmith.external.constants.spans.ActionSpans.ACTION_EXECUTION_CACHED_ACTION; +import static com.appsmith.external.constants.spans.ActionSpans.ACTION_EXECUTION_CACHED_DATASOURCE; +import static com.appsmith.external.constants.spans.ActionSpans.ACTION_EXECUTION_CACHED_PLUGIN; +import static com.appsmith.external.constants.spans.ActionSpans.ACTION_EXECUTION_DATASOURCE_CONTEXT; +import static com.appsmith.external.constants.spans.ActionSpans.ACTION_EXECUTION_DATASOURCE_CONTEXT_REMOTE; +import static com.appsmith.external.constants.spans.ActionSpans.ACTION_EXECUTION_EDITOR_CONFIG; +import static com.appsmith.external.constants.spans.ActionSpans.ACTION_EXECUTION_REQUEST_PARSING; +import static com.appsmith.external.constants.spans.ActionSpans.ACTION_EXECUTION_SERVER_EXECUTION; +import static com.appsmith.external.constants.spans.ActionSpans.ACTION_EXECUTION_VALIDATE_AUTHENTICATION; +import static com.appsmith.external.helpers.DataTypeStringUtils.getDisplayDataTypes; +import static com.appsmith.server.helpers.WidgetSuggestionHelper.getSuggestedWidgets; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +@Slf4j +public class ActionExecutionSolutionCEImpl implements ActionExecutionSolutionCE { + + private final NewActionService newActionService; + private final ActionPermission actionPermission; + private final ObservationRegistry observationRegistry; + private final ObjectMapper objectMapper; + private final NewActionRepository repository; + private final DatasourceService datasourceService; + private final PluginService pluginService; + private final DatasourceContextService datasourceContextService; + private final PluginExecutorHelper pluginExecutorHelper; + private final NewPageService newPageService; + private final ApplicationService applicationService; + private final SessionUserService sessionUserService; + private final AuthenticationValidator authenticationValidator; + private final DatasourcePermission datasourcePermission; + private final AnalyticsService analyticsService; + + static final String PARAM_KEY_REGEX = "^k\\d+$"; + static final String BLOB_KEY_REGEX = "^blob:[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}$"; + static final String EXECUTE_ACTION_DTO = "executeActionDTO"; + static final String PARAMETER_MAP = "parameterMap"; + List patternList = new ArrayList<>(); + + public ActionExecutionSolutionCEImpl(NewActionService newActionService, + ActionPermission actionPermission, + ObservationRegistry observationRegistry, + ObjectMapper objectMapper, + NewActionRepository repository, + DatasourceService datasourceService, + PluginService pluginService, + DatasourceContextService datasourceContextService, + PluginExecutorHelper pluginExecutorHelper, + NewPageService newPageService, + ApplicationService applicationService, + SessionUserService sessionUserService, + AuthenticationValidator authenticationValidator, + DatasourcePermission datasourcePermission, + AnalyticsService analyticsService) { + this.newActionService = newActionService; + this.actionPermission = actionPermission; + this.observationRegistry = observationRegistry; + this.objectMapper = objectMapper; + this.repository = repository; + this.datasourceService = datasourceService; + this.pluginService = pluginService; + this.datasourceContextService = datasourceContextService; + this.pluginExecutorHelper = pluginExecutorHelper; + this.newPageService = newPageService; + this.applicationService = applicationService; + this.sessionUserService = sessionUserService; + this.authenticationValidator = authenticationValidator; + this.datasourcePermission = datasourcePermission; + this.analyticsService = analyticsService; + + + this.patternList.add(Pattern.compile(PARAM_KEY_REGEX)); + this.patternList.add(Pattern.compile(BLOB_KEY_REGEX)); + this.patternList.add(Pattern.compile(EXECUTE_ACTION_DTO)); + this.patternList.add(Pattern.compile(PARAMETER_MAP)); + } + + /** + * Executes the action(queries) by creating executeActionDTO and sending it to the plugin for further execution + * + * @param partFlux + * @param branchName + * @param environmentName + * @return Mono of actionExecutionResult if the query succeeds, error messages otherwise + */ + @Override + public Mono executeAction(Flux partFlux, String branchName, String environmentName) { + return createExecuteActionDTO(partFlux) + .flatMap(executeActionDTO -> newActionService.findByBranchNameAndDefaultActionId(branchName, + executeActionDTO.getActionId(), + actionPermission.getExecutePermission()) + .map(branchedAction -> { + executeActionDTO.setActionId(branchedAction.getId()); + return executeActionDTO; + })) + .flatMap(executeActionDTO -> this.executeAction(executeActionDTO, environmentName)) + .name(ACTION_EXECUTION_SERVER_EXECUTION) + .tap(Micrometer.observation(observationRegistry)); + } + + /** + * Fetches the required Mono (action, datasource, and plugin) and makes actionExecution call to plugin + * + * @param executeActionDTO + * @param environmentName + * @return actionExecutionResult if query succeeds, error messages otherwise + */ + public Mono executeAction(ExecuteActionDTO executeActionDTO, String environmentName) { + + // 1. Validate input parameters which are required for mustache replacements + replaceNullWithQuotesForParamValues(executeActionDTO.getParams()); + + String actionId = executeActionDTO.getActionId(); + AtomicReference actionName = new AtomicReference<>(); + actionName.set(""); + + // 2. Fetch the action from the DB and check if it can be executed + Mono actionMono = getCachedActionForActionExecution(actionId); + Mono actionDTOMono = getCachedActionDTOForActionExecution(actionMono, executeActionDTO, actionId); + + // 3. Instantiate the implementation class based on the query type + Mono datasourceMono = getCachedDatasourceForActionExecution(actionDTOMono, environmentName); + Mono pluginMono = getCachedPluginForActionExecution(datasourceMono, actionId); + Mono pluginExecutorMono = pluginExecutorHelper.getPluginExecutor(pluginMono); + + // 4. Execute the query + Mono actionExecutionResultMono = getActionExecutionResult(executeActionDTO, + actionMono, + actionDTOMono, + datasourceMono, + pluginMono, + pluginExecutorMono, + actionName, + actionId, + environmentName); + + Mono editorConfigLabelMapMono = getEditorConfigLabelMap(datasourceMono); + + return actionExecutionResultMono + .zipWith(editorConfigLabelMapMono, (result, labelMap) -> { + if (TRUE.equals(executeActionDTO.getViewMode())) { + result.setRequest(null); + } else if (result.getRequest() != null && result.getRequest().getRequestParams() != null) { + transformRequestParams(result, labelMap); + } + return result; + }) + .map(result -> addDataTypesAndSetSuggestedWidget(result, executeActionDTO.getViewMode())); + } + + /** + * Creates the ExecuteActionDTO from Flux of ByteBuffers + * + * @param partFlux + * @return an executionDTO object with parameterMap + */ + protected Mono createExecuteActionDTO(Flux partFlux) { + final AtomicLong totalReadableByteCount = new AtomicLong(0); + final ExecuteActionDTO dto = new ExecuteActionDTO(); + return this.parsePartsAndGetParamsFlux(partFlux, totalReadableByteCount, dto) + .collectList() + .flatMap(params -> this.enrichExecutionParam(totalReadableByteCount, dto, params)) + .name(ACTION_EXECUTION_REQUEST_PARSING) + .tap(Micrometer.observation(observationRegistry)); + } + + /** + * This method attempts to parse all incoming parts by type, in parallel + * The expectation is that each part gets processed by the time this flux ends, + * and the DTO is updated accordingly + * + * @param partFlux Raw flux of parts as received in the execution request + * @param totalReadableByteCount An atomic type to store the total execution request size as and when we parse them + * @param dto The ExecuteActionDTO object to store all results in + * @return + */ + protected Flux parsePartsAndGetParamsFlux(Flux partFlux, AtomicLong totalReadableByteCount, ExecuteActionDTO dto) { + return partFlux + .groupBy(part -> { + // We're grouping parts by the type of processing required + // Expected types: meta, value, blob + + for (Pattern pattern : patternList) { + Matcher matcher = pattern.matcher(part.name()); + if (matcher.find()) { + return pattern.pattern(); + } + } + return part.name(); + }) + .flatMap(groupedPartsFlux -> { + String key = groupedPartsFlux.key(); + return switch (key) { + case PARAM_KEY_REGEX -> + groupedPartsFlux.flatMap(part -> this.parseExecuteParameter(part, totalReadableByteCount)); + case BLOB_KEY_REGEX -> + this.parseExecuteBlobs(groupedPartsFlux, dto, totalReadableByteCount).then(Mono.empty()); + case EXECUTE_ACTION_DTO -> + groupedPartsFlux.next().flatMap(part -> this.parseExecuteActionPart(part, dto)).then(Mono.empty()); + case PARAMETER_MAP -> + groupedPartsFlux.next().flatMap(part -> this.parseExecuteParameterMapPart(part, dto)).then(Mono.empty()); + default -> + Mono.error(new AppsmithException(AppsmithError.GENERIC_BAD_REQUEST, "Unexpected part found: " + key)); + }; + }); + } + + + protected Mono parseExecuteActionPart(Part part, ExecuteActionDTO dto) { + return DataBufferUtils + .join(part.content()) + .flatMap(executeActionDTOBuffer -> { + byte[] byteData = new byte[executeActionDTOBuffer.readableByteCount()]; + executeActionDTOBuffer.read(byteData); + DataBufferUtils.release(executeActionDTOBuffer); + try { + return Mono.just(objectMapper.readValue(byteData, ExecuteActionDTO.class)); + } catch (IOException e) { + log.error("Error in deserializing ExecuteActionDTO", e); + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, EXECUTE_ACTION_DTO)); + } + }) + .flatMap(executeActionDTO -> { + dto.setActionId(executeActionDTO.getActionId()); + dto.setViewMode(executeActionDTO.getViewMode()); + dto.setParamProperties(executeActionDTO.getParamProperties()); + dto.setPaginationField(executeActionDTO.getPaginationField()); + return Mono.empty(); + }); + } + + protected Mono parseExecuteParameterMapPart(Part part, ExecuteActionDTO dto) { + return DataBufferUtils + .join(part.content()) + .flatMap(parameterMapBuffer -> { + byte[] byteData = new byte[parameterMapBuffer.readableByteCount()]; + parameterMapBuffer.read(byteData); + DataBufferUtils.release(parameterMapBuffer); + try { + return Mono.just(objectMapper.readValue(byteData, new TypeReference>() { + })); + } catch (IOException e) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, PARAMETER_MAP)); + } + }) + .flatMap(paramMap -> { + dto.setParameterMap(paramMap); + return Mono.empty(); + }); + } + + protected Mono parseExecuteParameter(Part part, AtomicLong totalReadableByteCount) { + final Param param = new Param(); + param.setPseudoBindingName(part.name()); + return DataBufferUtils + .join(part.content()) + .map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + totalReadableByteCount.addAndGet(dataBuffer.readableByteCount()); + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + param.setValue(new String(bytes, StandardCharsets.UTF_8)); + return param; + }); + } + + protected Mono parseExecuteBlobs(Flux partsFlux, ExecuteActionDTO dto, AtomicLong totalReadableByteCount) { + Map blobMap = new HashMap<>(); + dto.setBlobValuesMap(blobMap); + + return partsFlux + .flatMap(part -> { + return DataBufferUtils + .join(part.content()) + .map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + totalReadableByteCount.addAndGet(dataBuffer.readableByteCount()); + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + blobMap.put(part.name(), new String(bytes, StandardCharsets.ISO_8859_1)); + return Mono.empty(); + }); + }) + .then(); + } + + + protected Mono enrichExecutionParam(AtomicLong totalReadableByteCount, ExecuteActionDTO dto, List params) { + if (dto.getActionId() == null) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ACTION_ID)); + } + + dto.setTotalReadableByteCount(totalReadableByteCount.longValue()); + + final Set visitedBindings = new HashSet<>(); + /* + Parts in multipart request can appear in any order. In order to avoid NPE original name of the parameters + along with the client-side data type are set here as it's guaranteed at this point that the part having the parameterMap is already collected. + Ref: https://github.com/appsmithorg/appsmith/issues/16722 + */ + params.forEach( + param -> { + String pseudoBindingName = param.getPseudoBindingName(); + String bindingValue = dto.getInvertParameterMap().get(pseudoBindingName); + param.setKey(bindingValue); + visitedBindings.add(bindingValue); + // if the type is not an array e.g. "k1": "string" or "k1": "boolean" + ParamProperty paramProperty = dto.getParamProperties().get(pseudoBindingName); + if (paramProperty != null) { + this.identifyExecutionParamDatatype(param, paramProperty); + + this.substituteBlobValuesInParam(dto, param, paramProperty); + } + + } + ); + + // In case there are parameters that did not receive a value in the multipart request, + // initialize these bindings with empty strings + if (dto.getParameterMap() != null) { + dto.getParameterMap() + .keySet() + .stream() + .forEach(parameter -> { + if (!visitedBindings.contains(parameter)) { + Param newParam = new Param(parameter, ""); + params.add(newParam); + } + }); + } + dto.setParams(params); + return Mono.just(dto); + } + + private void substituteBlobValuesInParam(ExecuteActionDTO dto, Param param, ParamProperty paramProperty) { + // Check if this param has blobUrlPaths + if (paramProperty.getBlobIdentifiers() != null && !paramProperty.getBlobIdentifiers().isEmpty()) { + // If it does, trigger the replacement logic for each of these urlPaths + String replacedValue = this.replaceBlobValuesInParam( + param.getValue(), + paramProperty.getBlobIdentifiers(), + dto.getBlobValuesMap()); + // And then update the value for this param + param.setValue(replacedValue); + } + } + + private void identifyExecutionParamDatatype(Param param, ParamProperty paramProperty) { + Object datatype = paramProperty.getDatatype(); + if (datatype instanceof String) { + param.setClientDataType(ClientDataType.valueOf(String.valueOf(datatype).toUpperCase())); + } else if (datatype instanceof LinkedHashMap) { + // if the type is an array e.g. "k1": { "array": [ "string", "number", "string", "boolean"] + LinkedHashMap stringArrayListLinkedHashMap = + (LinkedHashMap) datatype; + Optional firstKeyOpt = stringArrayListLinkedHashMap.keySet() + .stream() + .findFirst(); + if (firstKeyOpt.isPresent()) { + String firstKey = firstKeyOpt.get(); + param.setClientDataType(ClientDataType.valueOf(firstKey.toUpperCase())); + List individualTypes = stringArrayListLinkedHashMap.get(firstKey); + List dataTypesOfArrayElements = + individualTypes.stream() + .map(it -> ClientDataType.valueOf(String.valueOf(it) + .toUpperCase())) + .collect(Collectors.toList()); + param.setDataTypesOfArrayElements(dataTypesOfArrayElements); + } + } + } + + protected String replaceBlobValuesInParam(String value, List blobIdentifiers, Map blobValuesMap) { + // If there is no blobId reference against this param, return as is + if (blobIdentifiers == null || blobIdentifiers.isEmpty()) { + return value; + } + + // Otherwise, for each such blobId reference, replace the reference with the actual value from the blobMap + for (String blobId : blobIdentifiers) { + value = value.replace(blobId, StringEscapeUtils.escapeJava(blobValuesMap.get(blobId))); + } + + return value; + } + + /** + * Sets the param value to "" if key is not empty and value is null for each param + * + * @param params + */ + protected void replaceNullWithQuotesForParamValues(List params) { + + if (!CollectionUtils.isEmpty(params)) { + for (Param param : params) { + // In case the parameter values turn out to be null, set it to empty string instead to allow + // the execution to go through no matter what. + if (StringUtils.hasLength(param.getKey()) && param.getValue() == null) { + param.setValue(""); + } + } + } + } + + /** + * Fetches and caches action with permission. + * + * @param actionId + * @return actionMono + */ + protected Mono getCachedActionForActionExecution(String actionId) { + + return repository.findById(actionId, actionPermission.getExecutePermission()) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId))) + .name(ACTION_EXECUTION_CACHED_ACTION) + .tap(Micrometer.observation(observationRegistry)) + .cache(); + } + + /** + * Retrieves and caches validated actionDTO from actionMono. + * + * @param actionMono + * @param executeActionDTO + * @param actionId + * @return actionDTOMono + */ + protected Mono getCachedActionDTOForActionExecution(Mono actionMono, + ExecuteActionDTO executeActionDTO, + String actionId) { + return actionMono + .flatMap(action -> getValidActionForExecution(executeActionDTO, actionId, action)) + .cache(); + } + + /** + * Fetches, validates and caches the datasource from actionDTO + * + * @param actionDTOMono + * @return datasourceMono + */ + protected Mono getCachedDatasourceForActionExecution(Mono actionDTOMono, String environmentName) { + + return actionDTOMono + .flatMap(actionDTO -> datasourceService.getValidDatasourceFromActionMono(actionDTO, + datasourcePermission.getExecutePermission())) + .flatMap(datasource -> { + // For embedded datasource, validate the datasource for each execution + if (datasource.getId() == null) { + return datasourceService.validateDatasource(datasource); + } + + // The external datasource have already been validated. No need to validate again. + return Mono.just(datasource); + }) + .name(ACTION_EXECUTION_CACHED_DATASOURCE) + .tap(Micrometer.observation(observationRegistry)) + .cache(); + } + + /** + * fetches and caches plugin by pluginId after checking datasource for invalids(issues) + * + * @param datasourceMono + * @param actionId + * @return pluginMono if datasource has no issues and plugin is find, else throws error + */ + protected Mono getCachedPluginForActionExecution(Mono datasourceMono, String actionId) { + return datasourceMono + .flatMap(datasource -> { + Set invalids = datasource.getInvalids(); + if (!CollectionUtils.isEmpty(invalids)) { + log.error("Unable to execute actionId: {} because it's datasource is not valid. Cause: {}", + actionId, ArrayUtils.toString(invalids)); + return Mono.error(new AppsmithException(AppsmithError.INVALID_DATASOURCE, + datasource.getName(), + ArrayUtils.toString(invalids))); + } + return pluginService.findById(datasource.getPluginId()); + }) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PLUGIN))) + .name(ACTION_EXECUTION_CACHED_PLUGIN) + .tap(Micrometer.observation(observationRegistry)) + .cache(); + } + + /** + * Fetches and returns editorConfigLabelMap if datasourceId is present + * + * @param datasourceMono + * @return an Empty hashMap if datasource doesn't have id, else configLabelMap from plugin service + */ + protected Mono getEditorConfigLabelMap(Mono datasourceMono) { + + return datasourceMono + .flatMap(datasource -> { + if (!StringUtils.hasLength(datasource.getId())) { + return Mono.just(new HashMap()); + } + + return pluginService.getEditorConfigLabelMap(datasource.getPluginId()); + }) + .name(ACTION_EXECUTION_EDITOR_CONFIG) + .tap(Micrometer.observation(observationRegistry)); + } + + /** + * Passes the payload to pluginExecutor post datasource validation and context retrieval + *

+ * This method validates the datasource, retrieves context and subsequently passes the payload to pluginExecutor for + * further execution of the request. + *

+ *

In case of failure the method retries to from context

+ * + * @param executeActionDTO + * @param actionDTO + * @param datasource + * @param plugin + * @param pluginExecutor + * @param environmentName + * @return actionExecutionResultMono + */ + protected Mono verifyDatasourceAndMakeRequest(ExecuteActionDTO executeActionDTO, + ActionDTO actionDTO, + Datasource datasource, + Plugin plugin, + PluginExecutor pluginExecutor, + String environmentName) { + + DatasourceContextIdentifier dsContextIdentifier = new DatasourceContextIdentifier(); + + Mono executionMono = + datasourceService.getEvaluatedDSAndDsContextKeyWithEnvMap(datasource, environmentName) + .flatMap(tuple3 -> { + Datasource datasource1 = tuple3.getT1(); + DatasourceContextIdentifier datasourceContextIdentifier = tuple3.getT2(); + Map environmentMap = tuple3.getT3(); + + dsContextIdentifier.setDatasourceId(datasourceContextIdentifier.getDatasourceId()); + dsContextIdentifier.setEnvironmentId(datasourceContextIdentifier.getEnvironmentId()); + + return getValidatedDatasourceForActionExecution(datasource1, datasourceContextIdentifier.getEnvironmentId()) + .zipWhen(validatedDatasource -> getDsContextForActionExecution(validatedDatasource, + plugin, + datasourceContextIdentifier, + environmentMap)) + .flatMap(tuple2 -> { + Datasource validatedDatasource = tuple2.getT1(); + DatasourceContext resourceContext = tuple2.getT2(); + // Now that we have the context (connection details), execute the action. + + Instant requestedAt = Instant.now(); + return ((Mono) + pluginExecutor.executeParameterizedWithMetrics(resourceContext.getConnection(), + executeActionDTO, + validatedDatasource.getDatasourceConfiguration(), + actionDTO.getActionConfiguration(), + observationRegistry)) + .map(actionExecutionResult -> { + ActionExecutionRequest actionExecutionRequest = actionExecutionResult.getRequest(); + if (actionExecutionRequest == null) { + actionExecutionRequest = new ActionExecutionRequest(); + } + + actionExecutionRequest.setActionId(executeActionDTO.getActionId()); + actionExecutionRequest.setRequestedAt(requestedAt); + + actionExecutionResult.setRequest(actionExecutionRequest); + return actionExecutionResult; + }); + }); + }); + + return executionMono.onErrorResume(StaleConnectionException.class, error -> { + log.info("Looks like the connection is stale. Retrying with a fresh context."); + return deleteDatasourceContextForRetry(dsContextIdentifier).then(executionMono); + }); + } + + /** + * This is a composite method for fetching authenticated datasource, datasourceContextIdentifier, and environmentMap + * + * @param datasource + * @param environmentName + * @return + */ + protected Mono>> + getValidatedDatasourceWithDsContextKeyAndEnvMap(Datasource datasource, String environmentName) { + // see EE override for complete usage. + return datasourceService.getEvaluatedDSAndDsContextKeyWithEnvMap(datasource, environmentName) + .flatMap(tuple3 -> { + Datasource datasource1 = tuple3.getT1(); + DatasourceContextIdentifier datasourceContextIdentifier = tuple3.getT2(); + Map environmentMap = tuple3.getT3(); + + return getValidatedDatasourceForActionExecution(datasource1, environmentName) + .flatMap(datasource2 -> Mono.zip(Mono.just(datasource2), + Mono.just(datasourceContextIdentifier), + Mono.just(environmentMap)) + ); + }); + } + + /** + * Validates the datasource for further execution + * + * @param datasource + * @return + */ + protected Mono getValidatedDatasourceForActionExecution(Datasource datasource, String environmentId) { + // the environmentName argument is not consumed over here + // See EE override for usage of variable + return authenticationValidator.validateAuthentication(datasource, environmentId) + .name(ACTION_EXECUTION_VALIDATE_AUTHENTICATION) + .tap(Micrometer.observation(observationRegistry)) + .cache(); + } + + /** + * Provides datasource context for execution + * + * @param validatedDatasource + * @param plugin + * @param datasourceContextIdentifier + * @param environmentMap + * @return datasourceContextMono + */ + protected Mono> getDsContextForActionExecution(Datasource validatedDatasource, Plugin plugin, + DatasourceContextIdentifier datasourceContextIdentifier, + Map environmentMap) { + if (plugin.isRemotePlugin()) { + return datasourceContextService.getRemoteDatasourceContext(plugin, validatedDatasource) + .tag("plugin", plugin.getPackageName()) + .name(ACTION_EXECUTION_DATASOURCE_CONTEXT_REMOTE) + .tap(Micrometer.observation(observationRegistry)); + } + return datasourceContextService.getDatasourceContext(validatedDatasource, datasourceContextIdentifier, environmentMap) + .tag("plugin", plugin.getPackageName()) + .name(ACTION_EXECUTION_DATASOURCE_CONTEXT) + .tap(Micrometer.observation(observationRegistry)); + } + + /** + * Deletes the datasourceContext for the given datasource + * + * @param datasourceContextIdentifier + * @return datasourceContextMono + */ + protected Mono> deleteDatasourceContextForRetry(DatasourceContextIdentifier datasourceContextIdentifier) { + // the environmentName argument is not consumed over here + // See EE override for usage of variable + return datasourceContextService.deleteDatasourceContext(datasourceContextIdentifier); + } + + protected Mono handleExecutionErrors(Mono actionExecutionResultMono, + ActionDTO actionDTO, + Integer timeoutDuration, + String actionId) { + return actionExecutionResultMono + .onErrorMap(TimeoutException.class, error -> + new AppsmithPluginException(AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR, + actionDTO.getName(), + timeoutDuration)) + .onErrorMap(StaleConnectionException.class, error -> + new AppsmithPluginException(AppsmithPluginError.STALE_CONNECTION_ERROR)) + .onErrorResume(e -> { + log.debug("{}: In the action execution error mode.", + Thread.currentThread().getName(), e); + ActionExecutionResult result = new ActionExecutionResult(); + result.setErrorInfo(e); + result.setIsExecutionSuccess(false); + final ActionExecutionRequest actionExecutionRequest = new ActionExecutionRequest(); + actionExecutionRequest.setActionId(actionId); + actionExecutionRequest.setRequestedAt(Instant.now()); + result.setRequest(actionExecutionRequest); + return Mono.just(result); + }); + + } + + /** + * Handles the execution logic, call to pluginExecutor with the payload post retrieval and validation of action, datasource, and plugin + * + * @param executeActionDTO + * @param actionMono + * @param actionDTOMono + * @param datasourceMono + * @param pluginMono + * @param pluginExecutorMono + * @param actionName + * @param actionId + * @param environmentName + * @return actionExecutionResultMono + */ + protected Mono getActionExecutionResult(ExecuteActionDTO executeActionDTO, + Mono actionMono, + Mono actionDTOMono, + Mono datasourceMono, + Mono pluginMono, + Mono pluginExecutorMono, + AtomicReference actionName, + String actionId, + String environmentName) { + + Mono> executeActionPublishersCache = + Mono.zip(actionDTOMono, datasourceMono, pluginExecutorMono, pluginMono, actionMono).cache(); + + return executeActionPublishersCache + .flatMap(tuple -> { + final ActionDTO actionDTO = tuple.getT1(); + final Datasource datasource = tuple.getT2(); + final PluginExecutor pluginExecutor = tuple.getT3(); + final Plugin plugin = tuple.getT4(); + final NewAction actionFromDb = tuple.getT5(); + + // Set the action name + actionName.set(actionDTO.getName()); + + log.debug("[{}]Execute Action called in Page {}, for action id : {} action name : {}", + Thread.currentThread().getName(), + actionDTO.getPageId(), actionId, actionDTO.getName()); + + Integer timeoutDuration = actionDTO.getActionConfiguration().getTimeoutInMillisecond(); + + Mono actionExecutionResultMono = + verifyDatasourceAndMakeRequest(executeActionDTO, actionDTO, datasource, + plugin, pluginExecutor, environmentName) + .timeout(Duration.ofMillis(timeoutDuration)); + + return handleExecutionErrors(actionExecutionResultMono, actionDTO, timeoutDuration, actionId) + .elapsed() + // Now send the analytics event for this execution + .flatMap(tuple1 -> { + Long timeElapsed = tuple1.getT1(); + ActionExecutionResult result = tuple1.getT2(); + + log.debug("{}: Action {} with id {} execution time : {} ms", + Thread.currentThread().getName(), + actionName.get(), + actionId, + timeElapsed + ); + + return sendExecuteAnalyticsEvent(actionFromDb, actionDTO, datasource, + executeActionDTO, result, timeElapsed) + .then(Mono.just(result)); + }); + }) + .onErrorResume(AppsmithException.class, error -> { + ActionExecutionResult result = new ActionExecutionResult(); + result.setIsExecutionSuccess(false); + result.setErrorInfo(error); + return Mono.just(result); + }); + } + + + @Override + public Mono getValidActionForExecution(ExecuteActionDTO executeActionDTO, String actionId, NewAction newAction) { + Mono actionDTOMono = Mono.just(newAction) + .flatMap(dbAction -> { + ActionDTO action; + if (TRUE.equals(executeActionDTO.getViewMode())) { + action = dbAction.getPublishedAction(); + // If the action has not been published, return error + if (action == null) { + return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId)); + } + } else { + action = dbAction.getUnpublishedAction(); + } + + // Now check for erroneous situations which would deter the execution of the action : + + // Error out with in case of an invalid action + if (FALSE.equals(action.getIsValid())) { + return Mono.error(new AppsmithException( + AppsmithError.INVALID_ACTION, + action.getName(), + ArrayUtils.toString(action.getInvalids().toArray()) + )); + } + + // Error out in case of JS Plugin (this is currently client side execution only) + if (dbAction.getPluginType() == PluginType.JS) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + return Mono.just(action); + }); + return actionDTOMono; + } + + /** + * This function replaces the variables in the Object with the actual params + */ + @Override + public T variableSubstitution(T configuration, Map replaceParamsMap) { + return MustacheHelper.renderFieldValues(configuration, replaceParamsMap); + } + + /* + * - Get label for request params. + * - Transform request params list: [""] to a map: {"label": {"value": ...}} + * - Rearrange request params in the order as they appear in query editor form. + */ + private void transformRequestParams(ActionExecutionResult result, Map labelMap) { + Map transformedParams = new LinkedHashMap<>(); + Map requestParamsConfigMap = new HashMap(); + ((List) result.getRequest().getRequestParams()).stream() + .forEach(param -> requestParamsConfigMap.put(((RequestParamDTO) param).getConfigProperty(), + (RequestParamDTO) param)); + + labelMap.entrySet().stream() + .forEach(e -> { + String configProperty = e.getKey(); + if (requestParamsConfigMap.containsKey(configProperty)) { + RequestParamDTO param = requestParamsConfigMap.get(configProperty); + transformedParams.put(e.getValue(), param); + } + }); + + result.getRequest().setRequestParams(transformedParams); + } + + private ActionExecutionResult addDataTypesAndSetSuggestedWidget(ActionExecutionResult result, Boolean viewMode) { + + if (FALSE.equals(viewMode)) { + result.setSuggestedWidgets(getSuggestedWidgets(result.getBody())); + } + + /* + * - Do not process if data types are already present. + * - It means that data types have been added by specific plugin. + */ + + if (!CollectionUtils.isEmpty(result.getDataTypes())) { + return result; + } + + result.setDataTypes(getDisplayDataTypes(result.getBody())); + + return result; + } + + /** + * Since we're loading the application and other details from DB *only* for analytics, we check if analytics is + * active before making the call to DB. + * + * @return + */ + public Boolean isSendExecuteAnalyticsEvent() { + return analyticsService.isActive(); + } + + private Mono sendExecuteAnalyticsEvent( + NewAction action, + ActionDTO actionDTO, + Datasource datasource, + ExecuteActionDTO executeActionDto, + ActionExecutionResult actionExecutionResult, + Long timeElapsed + ) { + + if (!isSendExecuteAnalyticsEvent()) { + return Mono.empty(); + } + ActionExecutionRequest actionExecutionRequest = actionExecutionResult.getRequest(); + ActionExecutionRequest request; + if (actionExecutionRequest != null) { + // Do a deep copy of request to not edit + request = new ActionExecutionRequest( + actionExecutionRequest.getActionId(), + actionExecutionRequest.getRequestedAt(), + actionExecutionRequest.getQuery(), + actionExecutionRequest.getBody(), + actionExecutionRequest.getHeaders(), + actionExecutionRequest.getHttpMethod(), + actionExecutionRequest.getUrl(), + actionExecutionRequest.getProperties(), + actionExecutionRequest.getExecutionParameters(), + null + ); + } else { + request = new ActionExecutionRequest(); + } + + if (request.getHeaders() != null) { + JsonNode headers = objectMapper.convertValue(request.getHeaders(), JsonNode.class); + try { + final String headersAsString = objectMapper.writeValueAsString(headers); + request.setHeaders(headersAsString); + } catch (JsonProcessingException e) { + log.error(e.getMessage()); + } + } + + if (request.getBody() != null) { + try { + final String bodyAsString = objectMapper.writeValueAsString(request.getBody()); + request.setBody(bodyAsString); + } catch (JsonProcessingException e) { + log.error(e.getMessage()); + request.setBody("\"Error serializing value to JSON.\""); + } + } + + if (!CollectionUtils.isEmpty(request.getProperties())) { + final Map stringProperties = new HashMap<>(); + for (final Map.Entry entry : request.getProperties().entrySet()) { + String jsonValue; + try { + jsonValue = objectMapper.writeValueAsString(entry.getValue()); + } catch (JsonProcessingException e) { + jsonValue = "\"Error serializing value to JSON.\""; + } + stringProperties.put(entry.getKey(), jsonValue); + } + request.setProperties(stringProperties); + } + + return Mono.justOrEmpty(action.getApplicationId()) + .flatMap(applicationService::findById) + .defaultIfEmpty(new Application()) + .flatMap(application -> Mono.zip( + Mono.just(application), + sessionUserService.getCurrentUser(), + newPageService.getNameByPageId(actionDTO.getPageId(), executeActionDto.getViewMode()), + pluginService.getById(action.getPluginId()) + )) + .flatMap(tuple -> { + final Application application = tuple.getT1(); + final User user = tuple.getT2(); + final String pageName = tuple.getT3(); + final Plugin plugin = tuple.getT4(); + + final PluginType pluginType = action.getPluginType(); + final String appMode = TRUE.equals(executeActionDto.getViewMode()) ? ApplicationMode.PUBLISHED.toString() : ApplicationMode.EDIT.toString(); + + final Map data = new HashMap<>(Map.of( + "username", user.getUsername(), + "type", pluginType, + "pluginName", plugin.getName(), + "name", actionDTO.getName(), + "datasource", Map.of( + "name", datasource.getName() + ), + "orgId", application.getWorkspaceId(), + "appId", action.getApplicationId(), + FieldName.APP_MODE, appMode, + "appName", application.getName(), + "isExampleApp", application.isAppIsExample() + )); + + String dsCreatedAt = ""; + if (datasource.getCreatedAt() != null) { + dsCreatedAt = DateUtils.ISO_FORMATTER.format(datasource.getCreatedAt()); + } + List paramsList = executeActionDto.getParams(); + if (paramsList == null) { + paramsList = new ArrayList<>(); + } + List executionParams = paramsList.stream().map(param -> param.getValue()).collect(Collectors.toList()); + + data.putAll(Map.of( + "request", request, + "pageId", ObjectUtils.defaultIfNull(actionDTO.getPageId(), ""), + "pageName", pageName, + "isSuccessfulExecution", ObjectUtils.defaultIfNull(actionExecutionResult.getIsExecutionSuccess(), false), + "statusCode", ObjectUtils.defaultIfNull(actionExecutionResult.getStatusCode(), ""), + "timeElapsed", timeElapsed, + "actionCreated", DateUtils.ISO_FORMATTER.format(action.getCreatedAt()), + "actionId", ObjectUtils.defaultIfNull(action.getId(), "") + )); + data.putAll(Map.of( + FieldName.ACTION_EXECUTION_REQUEST_PARAMS_SIZE, executeActionDto.getTotalReadableByteCount(), + FieldName.ACTION_EXECUTION_REQUEST_PARAMS_COUNT, executionParams.size() + )); + + data.putAll( + Map.of( + "pluginErrorDetails", ObjectUtils.defaultIfNull(actionExecutionResult.getPluginErrorDetails(), "") + ) + ); + data.putAll(Map.of( + "dsId", ObjectUtils.defaultIfNull(datasource.getId(), ""), + "dsName", datasource.getName(), + "dsIsTemplate", ObjectUtils.defaultIfNull(datasource.getIsTemplate(), ""), + "dsIsMock", ObjectUtils.defaultIfNull(datasource.getIsMock(), ""), + "dsCreatedAt", dsCreatedAt + )); + + // Add the error message in case of erroneous execution + if (FALSE.equals(actionExecutionResult.getIsExecutionSuccess())) { + String errorJson; + try { + errorJson = objectMapper.writeValueAsString(actionExecutionResult.getBody()); + } catch (JsonProcessingException e) { + log.warn("Unable to serialize action execution error result to JSON.", e); + errorJson = "\"Failed to serialize error data to JSON.\""; + } + data.put("error", errorJson); + } + + if (actionExecutionResult.getStatusCode() != null) { + data.putAll(Map.of( + "statusCode", actionExecutionResult.getStatusCode() + )); + } + + String executionRequestQuery = ""; + if (actionExecutionResult != null && + actionExecutionResult.getRequest() != null && + actionExecutionResult.getRequest().getQuery() != null) { + executionRequestQuery = actionExecutionResult.getRequest().getQuery(); + } + + final Map eventData = new HashMap<>(Map.of( + FieldName.ACTION, action, + FieldName.DATASOURCE, datasource, + FieldName.APP_MODE, appMode, + FieldName.ACTION_EXECUTION_RESULT, actionExecutionResult, + FieldName.ACTION_EXECUTION_TIME, timeElapsed, + FieldName.ACTION_EXECUTION_QUERY, executionRequestQuery, + FieldName.APPLICATION, application, + FieldName.PLUGIN, plugin + )); + + if (executeActionDto.getTotalReadableByteCount() <= Constraint.MAX_ANALYTICS_SIZE_BYTES) { + // Only send params info if total size is less than 5 MB + eventData.put(FieldName.ACTION_EXECUTION_REQUEST_PARAMS, executionParams); + } else { + eventData.put(FieldName.ACTION_EXECUTION_REQUEST_PARAMS, REDACTED_DATA); + } + data.put(FieldName.EVENT_DATA, eventData); + + return analyticsService.sendObjectEvent(AnalyticsEvents.EXECUTE_ACTION, action, data) + .thenReturn(request); + }) + .onErrorResume(error -> { + log.warn("Error sending action execution data point", error); + return Mono.just(request); + }); + } +} diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ActionServiceCE_Test.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ActionServiceCE_Test.java index d9a6000626..7d731c8c47 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ActionServiceCE_Test.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/ActionServiceCE_Test.java @@ -1,26 +1,16 @@ package com.appsmith.server.services.ce; -import com.appsmith.external.constants.DisplayDataType; -import com.appsmith.external.dtos.ExecuteActionDTO; -import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; -import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; -import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.appsmith.external.helpers.AppsmithBeanUtils; import com.appsmith.external.helpers.AppsmithEventContext; import com.appsmith.external.helpers.AppsmithEventContextType; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionDTO; -import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.Datasource; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DefaultResources; -import com.appsmith.external.models.PaginationField; import com.appsmith.external.models.PaginationType; -import com.appsmith.external.models.ParsedDataType; import com.appsmith.external.models.Policy; import com.appsmith.external.models.Property; -import com.appsmith.external.models.WidgetSuggestionDTO; -import com.appsmith.external.models.WidgetType; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; @@ -37,13 +27,11 @@ import com.appsmith.server.dtos.ActionViewDTO; import com.appsmith.server.dtos.ApplicationAccessDTO; import com.appsmith.server.dtos.DslActionDTO; import com.appsmith.server.dtos.LayoutDTO; -import com.appsmith.server.dtos.MockDataSource; import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.MockPluginExecutor; import com.appsmith.server.helpers.PluginExecutorHelper; -import com.appsmith.server.helpers.WidgetSuggestionHelper; import com.appsmith.server.repositories.DatasourceRepository; import com.appsmith.server.repositories.PermissionGroupRepository; import com.appsmith.server.repositories.PluginRepository; @@ -61,8 +49,6 @@ import com.appsmith.server.services.PluginService; import com.appsmith.server.services.UserService; import com.appsmith.server.services.WorkspaceService; import com.appsmith.server.solutions.ImportExportApplicationService; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONArray; @@ -85,8 +71,6 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import reactor.util.function.Tuples; -import java.time.Duration; -import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -653,343 +637,7 @@ public class ActionServiceCE_Test { .verify(); } - @Test - @WithUserDetails(value = "api_user") - public void testVariableSubstitution() { - String json = "{\n" + - " \n" + - " \"deleted\": false,\n" + - " \"config\": {\n" + - " \"CONTAINER_WIDGET\": [\n" + - " {\n" + - " \"_id\": \"7\",\n" + - " \"sectionName\": \"General\",\n" + - " \"children\": [\n" + - " {\n" + - " \"_id\": \"7.1\",\n" + - " \"helpText\": \"Use a html color name, HEX, RGB or RGBA value\",\n" + - " \"placeholderText\": \"#FFFFFF / Gray / rgb(255, 99, 71)\",\n" + - " \"propertyName\": \"backgroundColor\",\n" + - " \"label\": \"Background Color\",\n" + - " \"controlType\": \"INPUT_TEXT\"\n" + - " },\n" + - " {\n" + - " \"_id\": \"7.2\",\n" + - " \"helpText\": \"Controls the visibility of the widget\",\n" + - " \"propertyName\": \"isVisible\",\n" + - " \"label\": \"Visible\",\n" + - " \"controlType\": \"SWITCH\",\n" + - " \"isJSConvertible\": true\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"name\": \"propertyPane\"\n" + - "}"; - ActionDTO action = new ActionDTO(); - action.setActionConfiguration(new ActionConfiguration()); - action.getActionConfiguration().setBody("{{Input.text}}"); - - ActionDTO renderedAction = newActionService.variableSubstitution(action, Map.of("Input.text", json)); - assertThat(renderedAction).isNotNull(); - assertThat(renderedAction.getActionConfiguration().getBody()).isEqualTo(json); - } - - @Test - @WithUserDetails(value = "api_user") - public void testVariableSubstitutionWithNewline() { - ActionDTO action = new ActionDTO(); - action.setActionConfiguration(new ActionConfiguration()); - action.getActionConfiguration().setBody("{{Input.text}}"); - - ActionDTO renderedAction = newActionService.variableSubstitution(action, Map.of("Input.text", "name\nvalue")); - assertThat(renderedAction).isNotNull(); - assertThat(renderedAction.getActionConfiguration().getBody()).isEqualTo("name\nvalue"); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecute() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("response-body"); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, List.of(new ParsedDataType(DisplayDataType.RAW))); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteNullRequestBody() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("response-body"); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.GET); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setName("testActionExecuteNullRequestBody"); - action.setPageId(testPage.getId()); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, List.of(new ParsedDataType(DisplayDataType.RAW))); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteDbQuery() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("response-body"); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("select * from users"); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecuteDbQuery"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteErrorResponse() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("response-body"); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHeaders(List.of( - new Property("random-header-key", "random-header-value"), - new Property("", "") - )); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecuteErrorResponse"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - AppsmithPluginException pluginException = new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR); - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.error(pluginException)); - Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); - - Mono executionResultMono = newActionService.executeAction(executeActionDTO, null); - - StepVerifier.create(executionResultMono) - .assertNext(result -> { - assertThat(result.getIsExecutionSuccess()).isFalse(); - assertThat(result.getStatusCode()).isEqualTo(pluginException.getAppErrorCode().toString()); - assertThat(result.getTitle()).isEqualTo(pluginException.getTitle()); - assertThat(result.getRequest().getActionId()).isEqualTo(createdAction.getId()); - assertThat(result.getRequest().getRequestedAt()).isBefore(Instant.now()); - }) - .verifyComplete(); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteNullPaginationParameters() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("response-body"); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHeaders(List.of( - new Property("random-header-key", "random-header-value"), - new Property("", "") - )); - actionConfiguration.setPaginationType(PaginationType.URL); - actionConfiguration.setNext(null); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecuteErrorResponse"); - DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); - datasource.setDatasourceConfiguration(datasourceConfiguration); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - executeActionDTO.setPaginationField(PaginationField.NEXT); - - AppsmithPluginException pluginException = new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR); - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.error(pluginException)); - Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); - - Mono executionResultMono = newActionService.executeAction(executeActionDTO, null); - - StepVerifier.create(executionResultMono) - .assertNext(result -> { - assertThat(result.getIsExecutionSuccess()).isFalse(); - assertThat(result.getStatusCode()).isEqualTo(pluginException.getAppErrorCode().toString()); - assertThat(result.getTitle()).isEqualTo(pluginException.getTitle()); - }) - .verifyComplete(); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteSecondaryStaleConnection() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("response-body"); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHeaders(List.of( - new Property("random-header-key", "random-header-value"), - new Property("", "") - )); - actionConfiguration.setTimeoutInMillisecond(String.valueOf(1000)); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecuteSecondaryStaleConnection"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) - .thenReturn(Mono.error(new StaleConnectionException())).thenReturn(Mono.error(new StaleConnectionException())); - Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); - - Mono executionResultMono = newActionService.executeAction(executeActionDTO, null); - - StepVerifier.create(executionResultMono) - .assertNext(result -> { - assertThat(result.getIsExecutionSuccess()).isFalse(); - assertThat(result.getStatusCode()).isEqualTo(AppsmithPluginError.STALE_CONNECTION_ERROR.getAppErrorCode()); - assertThat(result.getTitle()).isEqualTo(AppsmithPluginError.STALE_CONNECTION_ERROR.getTitle()); - }) - .verifyComplete(); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteTimeout() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("response-body"); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHeaders(List.of( - new Property("random-header-key", "random-header-value"), - new Property("", "") - )); - actionConfiguration.setTimeoutInMillisecond(String.valueOf(10)); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecuteTimeout"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) - .thenAnswer(x -> Mono.delay(Duration.ofMillis(1000)).ofType(ActionExecutionResult.class)); - Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); - - Mono executionResultMono = newActionService.executeAction(executeActionDTO, null); - - StepVerifier.create(executionResultMono) - .assertNext(result -> { - assertThat(result.getIsExecutionSuccess()).isFalse(); - assertThat(result.getStatusCode()).isEqualTo(AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR.getAppErrorCode().toString()); - assertThat(result.getTitle()).isEqualTo(AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR.getTitle()); - }) - .verifyComplete(); - } @Test @WithUserDetails(value = "api_user") @@ -1053,74 +701,6 @@ public class ActionServiceCE_Test { .verifyComplete(); } - @Test - @WithUserDetails(value = "api_user") - public void checkRecoveryFromStaleConnections() { - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("response-body"); - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) - .thenThrow(new StaleConnectionException()) - .thenReturn(Mono.just(mockResult)); - Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("select * from users"); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("checkRecoveryFromStaleConnections"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - Mono actionExecutionResultMono = newActionService.executeAction(executeActionDTO, null); - - StepVerifier.create(actionExecutionResultMono) - .assertNext(result -> { - assertThat(result).isNotNull(); - assertThat(result.getBody()).isEqualTo(mockResult.getBody()); - }) - .verifyComplete(); - } - - private void executeAndAssertAction(ExecuteActionDTO executeActionDTO, ActionConfiguration actionConfiguration, - ActionExecutionResult mockResult, List expectedReturnDataTypes) { - - List expectedWidgets = mockResult.getSuggestedWidgets(); - Mono actionExecutionResultMono = executeAction(executeActionDTO, actionConfiguration, mockResult); - - StepVerifier.create(actionExecutionResultMono) - .assertNext(result -> { - assertThat(result).isNotNull(); - assertThat(result.getBody()).isEqualTo(mockResult.getBody()); - assertThat(result.getDataTypes()).hasToString(expectedReturnDataTypes.toString()); - assertThat(result.getSuggestedWidgets()) - .usingRecursiveFieldByFieldElementComparator() - .containsExactlyInAnyOrderElementsOf(expectedWidgets); - assertThat(result.getRequest().getActionId()).isEqualTo(executeActionDTO.getActionId()); - assertThat(result.getRequest().getRequestedAt()).isBefore(Instant.now()); - }) - .verifyComplete(); - } - - private Mono executeAction(ExecuteActionDTO executeActionDTO, ActionConfiguration actionConfiguration, ActionExecutionResult mockResult) { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.just(mockResult)); - Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); - - Mono actionExecutionResultMono = newActionService.executeAction(executeActionDTO, null); - return actionExecutionResultMono; - } - @Test @WithUserDetails(value = "api_user") public void getActionInViewMode() { @@ -1154,48 +734,6 @@ public class ActionServiceCE_Test { .verifyComplete(); } - @Test - @WithUserDetails(value = "api_user") - public void executeActionWithExternalDatasource() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); - Mockito.when(pluginService.getEditorConfigLabelMap(Mockito.anyString())).thenReturn(Mono.just(new HashMap<>())); - - Datasource externalDatasource = new Datasource(); - externalDatasource.setName("Default Database"); - externalDatasource.setWorkspaceId(workspaceId); - Plugin restApiPlugin = pluginRepository.findByPackageName("restapi-plugin").block(); - externalDatasource.setPluginId(restApiPlugin.getId()); - DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); - datasourceConfiguration.setUrl("some url here"); - externalDatasource.setDatasourceConfiguration(datasourceConfiguration); - Datasource savedDs = datasourceService.create(externalDatasource).block(); - - ActionDTO action = new ActionDTO(); - action.setName("actionWithExternalDatasource"); - action.setPageId(testPage.getId()); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.GET); - action.setActionConfiguration(actionConfiguration); - action.setDatasource(savedDs); - - - Mono resultMono = layoutActionService.createSingleAction(action, Boolean.FALSE) - .flatMap(savedAction -> { - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(savedAction.getId()); - executeActionDTO.setViewMode(false); - return newActionService.executeAction(executeActionDTO, null); - }); - - - StepVerifier - .create(resultMono) - .assertNext(result -> { - assertThat(result).isNotNull(); - assertThat(result.getStatusCode()).isEqualTo("200"); - }) - .verifyComplete(); - } @Test @WithUserDetails(value = "api_user") @@ -1547,1108 +1085,6 @@ public class ActionServiceCE_Test { .verifyComplete(); } - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteWithTableReturnType() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("[\n" + - "{\"name\": \"Richard\", \"profession\": \"medical\"},\n" + - "{\"name\": \"John\", \"profession\": \"self employed\"},\n" + - "{\"name\": \"Mary\", \"profession\": \"engineer\"}\n" + - "]"); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.TABLE), new ParsedDataType(DisplayDataType.JSON) - , new ParsedDataType(DisplayDataType.RAW))); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteWithJsonReturnType() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("{\n" + - " \"name\":\"John\",\n" + - " \"age\":30,\n" + - " \"cars\": {\n" + - " \"car1\":\"Ford\",\n" + - " \"car2\":\"BMW\",\n" + - " \"car3\":\"Fiat\"\n" + - " }\n" + - " }"); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.JSON), new ParsedDataType(DisplayDataType.RAW))); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteWithPreAssignedReturnType() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("{\n" + - " \"name\":\"John\",\n" + - " \"age\":30,\n" + - " \"cars\": {\n" + - " \"car1\":\"Ford\",\n" + - " \"car2\":\"BMW\",\n" + - " \"car3\":\"Fiat\"\n" + - " }\n" + - " }"); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - } - - @Test - @WithUserDetails(value = "api_user") - public void testActionExecuteReturnTypeWithNullResultBody() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(null); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - - List widgetTypeList = new ArrayList<>(); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, new ArrayList<>()); - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithChartWidgetData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\": [\n" + - " {\n" + - " \"x\": \"Mon\",\n" + - " \"y\": 10000\n" + - " },\n" + - " {\n" + - " \"x\": \"Tue\",\n" + - " \"y\": 12000\n" + - " },\n" + - " {\n" + - " \"x\": \"Wed\",\n" + - " \"y\": 32000\n" + - " },\n" + - " {\n" + - " \"x\": \"Thu\",\n" + - " \"y\": 28000\n" + - " },\n" + - " {\n" + - " \"x\": \"Fri\",\n" + - " \"y\": 14000\n" + - " },\n" + - " {\n" + - " \"x\": \"Sat\",\n" + - " \"y\": 19000\n" + - " },\n" + - " {\n" + - " \"x\": \"Sun\",\n" + - " \"y\": 36000\n" + - " }\n" + - "]}"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "x", "y")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "x", "x")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithTableWidgetData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\": [\n" + - "\t{\n" + - "\t\t\"id\": \"0001\",\n" + - "\t\t\"type\": \"donut\",\n" + - "\t\t\"name\": \"Cake\",\n" + - "\t\t\"ppu\": 0.55,\n" + - "\t\t\"batters\":\n" + - "\t\t\t{\n" + - "\t\t\t\t\"batter\":\n" + - "\t\t\t\t\t[\n" + - "\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" },\n" + - "\t\t\t\t\t\t{ \"id\": \"1002\", \"type\": \"Chocolate\" },\n" + - "\t\t\t\t\t\t{ \"id\": \"1003\", \"type\": \"Blueberry\" },\n" + - "\t\t\t\t\t\t{ \"id\": \"1004\", \"type\": \"Devil's Food\" }\n" + - "\t\t\t\t\t]\n" + - "\t\t\t},\n" + - "\t\t\"topping\":\n" + - "\t\t\t[\n" + - "\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" + - "\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" + - "\t\t\t\t{ \"id\": \"5005\", \"type\": \"Sugar\" },\n" + - "\t\t\t\t{ \"id\": \"5007\", \"type\": \"Powdered Sugar\" },\n" + - "\t\t\t\t{ \"id\": \"5006\", \"type\": \"Chocolate with Sprinkles\" },\n" + - "\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" + - "\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" + - "\t\t\t]\n" + - "\t},\n" + - "\t{\n" + - "\t\t\"id\": \"0002\",\n" + - "\t\t\"type\": \"donut\",\n" + - "\t\t\"name\": \"Raised\",\n" + - "\t\t\"ppu\": 0.55,\n" + - "\t\t\"batters\":\n" + - "\t\t\t{\n" + - "\t\t\t\t\"batter\":\n" + - "\t\t\t\t\t[\n" + - "\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" }\n" + - "\t\t\t\t\t]\n" + - "\t\t\t},\n" + - "\t\t\"topping\":\n" + - "\t\t\t[\n" + - "\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" + - "\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" + - "\t\t\t\t{ \"id\": \"5005\", \"type\": \"Sugar\" },\n" + - "\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" + - "\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" + - "\t\t\t]\n" + - "\t},\n" + - "\t{\n" + - "\t\t\"id\": \"0003\",\n" + - "\t\t\"type\": \"donut\",\n" + - "\t\t\"name\": \"Old Fashioned\",\n" + - "\t\t\"ppu\": 0.55,\n" + - "\t\t\"batters\":\n" + - "\t\t\t{\n" + - "\t\t\t\t\"batter\":\n" + - "\t\t\t\t\t[\n" + - "\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" },\n" + - "\t\t\t\t\t\t{ \"id\": \"1002\", \"type\": \"Chocolate\" }\n" + - "\t\t\t\t\t]\n" + - "\t\t\t},\n" + - "\t\t\"topping\":\n" + - "\t\t\t[\n" + - "\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" + - "\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" + - "\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" + - "\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" + - "\t\t\t]\n" + - "\t}\n" + - "]}"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "id", "ppu")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "id", "type")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithListWidgetData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\": [\n" + - " {\n" + - " \"url\": \"images/thumbnails/0001.jpg\",\n" + - " \"width\": 32,\n" + - " \"height\": 32\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0001.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0002.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0002.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0003.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0004.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0005.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0006.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0007.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0008.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0009.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " },\n" + - " {\n" + - " \"url\": \"images/0010.jpg\",\n" + - " \"width\": 200,\n" + - " \"height\": 200\n" + - " }\n" + - "]}"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithDropdownWidgetData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\": [\n" + - " {\n" + - " \"CarType\": \"BMW\",\n" + - " \"carID\": \"bmw123\"\n" + - " },\n" + - " {\n" + - " \"CarType\": \"mercedes\",\n" + - " \"carID\": \"merc123\"\n" + - " },\n" + - " {\n" + - " \"CarType\": \"volvo\",\n" + - " \"carID\": \"vol123r\"\n" + - " },\n" + - " {\n" + - " \"CarType\": \"ford\",\n" + - " \"carID\": \"ford123\"\n" + - " }\n" + - " ]}"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "CarType", "carID")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithArrayOfStringsDropDownWidget() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\":[\"string1\", \"string2\", \"string3\", \"string4\"] }"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.INPUT_WIDGET)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithArrayOfArray() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\":[[\"string1\", \"string2\", \"string3\", \"string4\"]," + - "[\"string5\", \"string6\", \"string7\", \"string8\"]," + - "[\"string9\", \"string10\", \"string11\", \"string12\"]] }"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithEmptyData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\":[] }"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithNumericData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\": [1] }"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.INPUT_WIDGET)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithJsonNodeData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{\"data\": {\n" + - " \"next\": \"https://mock-api.appsmith.com/users?page=2&pageSize=10\",\n" + - " \"previous\": null,\n" + - " \"users\": [\n" + - " {\n" + - " \"id\": 3,\n" + - " \"name\": \"Demetre\",\n" + - " \"status\": \"APPROVED\",\n" + - " \"gender\": \"Male\",\n" + - " \"avatar\": \"https://robohash.org/iustooptiocum.jpg?size=100x100&set=set1\",\n" + - " \"email\": \"aaaa@bbb.com\",\n" + - " \"address\": \"262 Saint Paul Park\",\n" + - " \"createdAt\": \"2020-05-01T17:30:50.000Z\",\n" + - " \"updatedAt\": \"2019-10-08T14:55:53.000Z\"\n" + - " },\n" + - " {\n" + - " \"id\": 4,\n" + - " \"name\": \"Currey\",\n" + - " \"status\": \"APPROVED\",\n" + - " \"gender\": \"Female\",\n" + - " \"avatar\": \"https://robohash.org/aspernaturnatusrepellat.jpg?size=100x100&set=set1\",\n" + - " \"email\": \"cbrayson3@taobao.com\",\n" + - " \"address\": \"35180 Lotheville Street!\",\n" + - " \"createdAt\": \"2019-12-30T03:54:23.000Z\",\n" + - " \"updatedAt\": \"2020-08-12T17:43:01.016Z\"\n" + - " }\n" + - " ]\n" + - "}}"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, "users")); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.CHART_WIDGET, "users", "name", "id")); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, "users")); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.SELECT_WIDGET, "users", "name", "status")); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithJsonObjectData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\": {\n" + - " \"id\": 1,\n" + - " \"name\": \"Barty Crouch\",\n" + - " \"status\": \"APPROVED\",\n" + - " \"gender\": \"\",\n" + - " \"avatar\": \"https://robohash.org/sednecessitatibuset.png?size=100x100&set=set1\",\n" + - " \"email\": \"barty.crouch@gmail.com\",\n" + - " \"address\": \"St Petersberg #911 4th main\",\n" + - " \"createdAt\": \"2020-03-16T18:00:05.000Z\",\n" + - " \"updatedAt\": \"2020-08-12T17:29:31.980Z\"\n" + - " } }"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionAfterExecutionWithJsonArrayObjectData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{ \"data\": {\n" + - " \"id\": 1,\n" + - " \"name\": \"Barty Crouch\",\n" + - " \"status\": \"APPROVED\",\n" + - " \"gender\": \"\",\n" + - " \"avatar\": \"https://robohash.org/sednecessitatibuset.png?size=100x100&set=set1\",\n" + - " \"email\": \"barty.crouch@gmail.com\",\n" + - " \"address\": \"St Petersberg #911 4th main\",\n" + - " \"createdAt\": \"2020-03-16T18:00:05.000Z\",\n" + - " \"updatedAt\": \"2020-08-12T17:29:31.980Z\"\n" + - " }," + - "\"data\": {\n" + - " \"id\": 2,\n" + - " \"name\": \"Jenelle Kibbys\",\n" + - " \"status\": \"APPROVED\",\n" + - " \"gender\": \"Female\",\n" + - " \"avatar\": \"https://robohash.org/quiaasperiorespariatur.bmp?size=100x100&set=set1\",\n" + - " \"email\": \"jkibby1@hp.com\",\n" + - " \"address\": \"85 Tennessee Plaza\",\n" + - " \"createdAt\": \"2019-10-04T03:22:23.000Z\",\n" + - " \"updatedAt\": \"2019-09-11T20:18:38.000Z\"\n" + - " } }"; - final JsonNode arrNode = new ObjectMapper().readTree(data); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionNestedData() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{\"data\": {\n" + - " \"next\": \"https://mock-api.appsmith.com/users?page=2&pageSize=10\",\n" + - " \"previous\": null,\n" + - " \"users\": [1, 2, 3]\n" + - "}}"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, "users")); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, "users")); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void testWidgetSuggestionNestedDataEmpty() throws JsonProcessingException { - - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - final String data = "{\"data\": {\n" + - " \"next\": \"https://mock-api.appsmith.com/users?page=2&pageSize=10\",\n" + - " \"previous\": null,\n" + - " \"users\": []\n" + - "}}"; - final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); - ; - - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(arrNode); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, null)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } - - @Test - @WithUserDetails(value = "api_user") - public void suggestWidget_ArrayListData_SuggestTableTextChartDropDownWidget() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - ArrayList listData = new ArrayList<>(); - JSONObject jsonObject = new JSONObject(Map.of("url", "images/thumbnails/0001.jpg", "width", 32, "height", 32)); - listData.add(jsonObject); - jsonObject = new JSONObject(Map.of("url", "images/0001.jpg", "width", 42, "height", 22)); - listData.add(jsonObject); - jsonObject = new JSONObject(Map.of("url", "images/0002.jpg", "width", 52, "height", 12)); - listData.add(jsonObject); - jsonObject = new JSONObject(Map.of("url", "images/0003.jpg", "width", 62, "height", 52)); - listData.add(jsonObject); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(listData); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - } - - @Test - @WithUserDetails(value = "api_user") - public void suggestWidget_ArrayListData_SuggestTableTextDropDownWidget() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - ArrayList listData = new ArrayList<>(); - JSONObject jsonObject = new JSONObject(Map.of("url", "images/thumbnails/0001.jpg", "width", "32", "height", "32")); - listData.add(jsonObject); - jsonObject = new JSONObject(Map.of("url", "images/0001.jpg", "width", "42", "height", "22")); - listData.add(jsonObject); - jsonObject = new JSONObject(Map.of("url", "images/0002.jpg", "width", "52", "height", "12")); - listData.add(jsonObject); - jsonObject = new JSONObject(Map.of("url", "images/0003.jpg", "width", "62", "height", "52")); - listData.add(jsonObject); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(listData); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "width", "url")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - } - - @Test - @WithUserDetails(value = "api_user") - public void suggestWidget_ArrayListDataEmpty_SuggestTextWidget() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - ActionExecutionResult mockResult = new ActionExecutionResult(); - ArrayList listData = new ArrayList<>(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody(listData); - mockResult.setStatusCode("200"); - mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setHttpMethod(HttpMethod.POST); - actionConfiguration.setBody("random-request-body"); - actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecute"); - action.setDatasource(datasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - - } private Mono createPage(Application app, PageDTO page) { return newPageService @@ -2789,47 +1225,6 @@ public class ActionServiceCE_Test { .verifyComplete(); } - @Test - @WithUserDetails(value = "api_user") - public void executeAction_actionOnMockDatasource_success() { - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); - Mockito.when(pluginService.getEditorConfigLabelMap(Mockito.anyString())).thenReturn(Mono.just(new HashMap<>())); - Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) - .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("response-body"); - - Plugin installed_plugin = pluginRepository.findByPackageName("restapi-plugin").block(); - MockDataSource mockDataSource = new MockDataSource(); - mockDataSource.setName("Users"); - mockDataSource.setWorkspaceId(workspaceId); - mockDataSource.setPackageName("postgres-plugin"); - mockDataSource.setPluginId(installed_plugin.getId()); - Datasource mockDatasource = mockDataService.createMockDataSet(mockDataSource).block(); - - List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); - mockResult.setSuggestedWidgets(widgetTypeList); - - ActionDTO action = new ActionDTO(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("select * from users"); - action.setActionConfiguration(actionConfiguration); - action.setPageId(testPage.getId()); - action.setName("testActionExecuteDbQuery"); - action.setDatasource(mockDatasource); - ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId(createdAction.getId()); - executeActionDTO.setViewMode(false); - - executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, - List.of(new ParsedDataType(DisplayDataType.RAW))); - } - @Test @WithUserDetails("api_user") public void validateAndSaveActionToRepository_noDatasourceEditPermission() { diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceCEImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceCEImplTest.java index c42a664000..439e8c8209 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceCEImplTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceCEImplTest.java @@ -124,13 +124,10 @@ public class NewActionServiceCEImplTest { ConfigService configService; @MockBean ResponseUtils responseUtils; - @MockBean PermissionGroupService permissionGroupService; - @MockBean NewActionRepository newActionRepository; - @MockBean DatasourcePermission datasourcePermission; @MockBean @@ -142,10 +139,6 @@ public class NewActionServiceCEImplTest { @MockBean ObservationRegistry observationRegistry; - private BodyExtractor.Context context; - - private Map hints; - @BeforeEach public void setup() { newActionService = new NewActionServiceCEImpl(scheduler, @@ -156,15 +149,12 @@ public class NewActionServiceCEImplTest { analyticsService, datasourceService, pluginService, - datasourceContextService, pluginExecutorHelper, marketplaceService, policyGenerator, newPageService, applicationService, - sessionUserService, policyUtils, - authenticationValidator, configService, responseUtils, permissionGroupService, @@ -178,98 +168,6 @@ public class NewActionServiceCEImplTest { Mockito.when(observationRegistry.observationConfig()).thenReturn(mockObservationConfig); } - @BeforeEach - public void createContext() { - final List> messageReaders = new ArrayList<>(); - messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); - messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); - messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); - messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); - messageReaders.add(new FormHttpMessageReader()); - DefaultPartHttpMessageReader partReader = new DefaultPartHttpMessageReader(); - messageReaders.add(partReader); - messageReaders.add(new MultipartHttpMessageReader(partReader)); - - this.context = new BodyExtractor.Context() { - @Override - public List> messageReaders() { - return messageReaders; - } - - @Override - public Optional serverResponse() { - return Optional.empty(); - } - - @Override - public Map hints() { - return hints; - } - }; - this.hints = new HashMap<>(); - } - - @Test - public void testExecuteAction_withoutExecuteActionDTOPart_failsValidation() { - final Mono actionExecutionResultMono = newActionService.executeAction(Flux.empty(), null, null); - - StepVerifier - .create(actionExecutionResultMono) - .expectErrorMatches(e -> e instanceof AppsmithException && - e.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.ACTION_ID))) - .verify(); - } - - @Test - public void testExecuteAction_withMalformedExecuteActionDTO_failsValidation() { - MockServerHttpRequest mock = MockServerHttpRequest - .method(HttpMethod.POST, URI.create("https://example.com")) - .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) - .body(""" - --boundary\r - Content-Disposition: form-data; name="executeActionDTO"\r - \r - irrelevant content\r - --boundary--\r - """); - - final Flux partsFlux = BodyExtractors.toParts() - .extract(mock, this.context); - - final Mono actionExecutionResultMono = newActionService.executeAction(partsFlux, null, null); - - StepVerifier - .create(actionExecutionResultMono) - .expectErrorMatches(e -> e instanceof AppsmithException && - e.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage("executeActionDTO"))) - .verify(); - } - - @Test - public void testExecuteAction_withoutActionId_failsValidation() { - MockServerHttpRequest mock = MockServerHttpRequest - .method(HttpMethod.POST, URI.create("https://example.com")) - .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) - .body(""" - --boundary\r - Content-Disposition: form-data; name="executeActionDTO"\r - \r - {"viewMode":false}\r - --boundary--\r - """); - - final Flux partsFlux = BodyExtractors.toParts() - .extract(mock, this.context); - - final Mono actionExecutionResultMono = newActionService.executeAction(partsFlux, null, null); - - StepVerifier - .create(actionExecutionResultMono) - .expectErrorMatches(e -> e instanceof AppsmithException && - e.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.ACTION_ID))) - .verify(); - } - @Test public void testMissingPluginIdAndTypeFixForNonJSPluginType() { /* Mock `findById` method of pluginService to return `testPlugin` */ @@ -324,176 +222,4 @@ public class NewActionServiceCEImplTest { .verifyComplete(); } - @Test - public void testExecuteAPIWithUsualOrderingOfTheParts() { - String usualOrderOfParts = """ - --boundary\r - Content-Disposition: form-data; name="executeActionDTO"\r - \r - {"actionId":"63285a3388e48972c7519b18","viewMode":false,"paramProperties":{"k0":{"datatype": "string"}}}\r - --boundary\r - Content-Disposition: form-data; name="parameterMap"\r - \r - {"Input1.text":"k0"}\r - --boundary\r - Content-Disposition: form-data; name="k0"; filename="blob"\r - Content-Type: text/plain\r - \r - xyz\r - --boundary--"""; - - MockServerHttpRequest mock = MockServerHttpRequest - .method(HttpMethod.POST, URI.create("https://example.com")) - .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) - .body(usualOrderOfParts); - - final Flux partsFlux = BodyExtractors.toParts() - .extract(mock, this.context); - - NewActionServiceCE newActionServiceSpy = spy(newActionService); - - Mono actionExecutionResultMono = newActionServiceSpy.executeAction(partsFlux, null, null); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("test body"); - mockResult.setTitle("test title"); - - NewAction newAction = new NewAction(); - newAction.setId("63285a3388e48972c7519b18"); - doReturn(Mono.just(mockResult)).when(newActionServiceSpy).executeAction(any(), any()); - doReturn(Mono.just(newAction)).when(newActionServiceSpy).findByBranchNameAndDefaultActionId(any(), any(), any()); - - - StepVerifier - .create(actionExecutionResultMono) - .assertNext(response -> { - assertNotNull(response); - assertTrue(response.getIsExecutionSuccess()); - assertEquals(mockResult.getBody().toString(), response.getBody().toString()); - }) - .verifyComplete(); - } - - @Test - public void testExecuteAPIWithParameterMapAsLastPart() { - String parameterMapAtLast = """ - --boundary\r - Content-Disposition: form-data; name="executeActionDTO"\r - \r - {"actionId":"63285a3388e48972c7519b18","viewMode":false,"paramProperties":{"k0":{"datatype": "string"}}}\r - --boundary\r - Content-Disposition: form-data; name="k0"; filename="blob"\r - Content-Type: text/plain\r - \r - xyz\r - --boundary\r - Content-Disposition: form-data; name="parameterMap"\r - \r - {"Input1.text":"k0"}\r - --boundary--"""; - - MockServerHttpRequest mock = MockServerHttpRequest - .method(HttpMethod.POST, URI.create("https://example.com")) - .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) - .body(parameterMapAtLast); - - final Flux partsFlux = BodyExtractors.toParts() - .extract(mock, this.context); - - NewActionServiceCE newActionServiceSpy = spy(newActionService); - - Mono actionExecutionResultMono = newActionServiceSpy.executeAction(partsFlux, null, null); - - ActionExecutionResult mockResult = new ActionExecutionResult(); - mockResult.setIsExecutionSuccess(true); - mockResult.setBody("test body"); - mockResult.setTitle("test title"); - - NewAction newAction = new NewAction(); - newAction.setId("63285a3388e48972c7519b18"); - doReturn(Mono.just(mockResult)).when(newActionServiceSpy).executeAction(any(), any()); - doReturn(Mono.just(newAction)).when(newActionServiceSpy).findByBranchNameAndDefaultActionId(any(), any(), any()); - - - StepVerifier - .create(actionExecutionResultMono) - .assertNext(response -> { - assertNotNull(response); - assertTrue(response.getIsExecutionSuccess()); - assertEquals(mockResult.getBody().toString(), response.getBody().toString()); - }) - .verifyComplete(); - } - - @Test - public void testParsePartsAndGetParamsFlux_withBlobIdentifiers_replacesValueInParam() { - String partsWithBlobRefs = """ - --boundary\r - Content-Disposition: form-data; name="executeActionDTO"\r - \r - {"actionId":"63285a3388e48972c7519b18","viewMode":false,"paramProperties":{"k0":{"datatype": "string", "blobIdentifiers": ["blob:12345678-1234-1234-1234-123456781234"]}}}\r - --boundary\r - Content-Disposition: form-data; name="parameterMap"\r - \r - {"Input1.text":"k0"}\r - --boundary\r - Content-Disposition: form-data; name="k0"; filename="blob"\r - Content-Type: text/plain\r - \r - {"name": "randomName", "data": "blob:12345678-1234-1234-1234-123456781234"}\r - --boundary\r - Content-Disposition: form-data; name="blob:12345678-1234-1234-1234-123456781234"; filename="blob"\r - Content-Type: text/plain\r - \r - xy\\nz\r - --boundary--"""; - - MockServerHttpRequest mock = MockServerHttpRequest - .method(HttpMethod.POST, URI.create("https://example.com")) - .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) - .body(partsWithBlobRefs); - - final Flux partsFlux = BodyExtractors.toParts() - .extract(mock, this.context); - - AtomicLong atomicLong = new AtomicLong(); - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - Mono> paramsListMono = newActionService.parsePartsAndGetParamsFlux(partsFlux, atomicLong, executeActionDTO).collectList().cache(); - - StepVerifier.create(paramsListMono) - .assertNext(paramsList -> { - assertEquals(1, paramsList.size()); - Param param = paramsList.get(0); - assertEquals("{\"name\": \"randomName\", \"data\": \"blob:12345678-1234-1234-1234-123456781234\"}", param.getValue()); - }) - .verifyComplete(); - } - - @Test - public void testEnrichExecutionParams_withBlobReference_performsSubstitutionCorrectly() { - AtomicLong atomicLong = new AtomicLong(45L); - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setActionId("testId"); - executeActionDTO.setViewMode(false); - executeActionDTO.setParamProperties(Map.of("k0", new ParamProperty("string", List.of("blobId")))); - executeActionDTO.setParameterMap(Map.of("Input1.text", "k0")); - executeActionDTO.setBlobValuesMap(Map.of("blobId", "xy\\nz")); - Param param1 = new Param(); - param1.setValue("{\"name\": \"randomName\", \"data\": \"blobId\"}"); - param1.setPseudoBindingName("k0"); - List params = List.of(param1); - - Mono enrichedDto = newActionService.enrichExecutionParam(atomicLong, executeActionDTO, params); - - StepVerifier.create(enrichedDto) - .assertNext(dto -> { - assertEquals(45, dto.getTotalReadableByteCount()); - - Param param = dto.getParams().get(0); - assertEquals(ClientDataType.STRING, param.getClientDataType()); - assertEquals("{\"name\": \"randomName\", \"data\": \"xy\\\\nz\"}", param.getValue()); - }) - .verifyComplete(); - } } \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImplTest.java new file mode 100644 index 0000000000..7374da53cf --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCEImplTest.java @@ -0,0 +1,404 @@ +package com.appsmith.server.solutions.ce; + +import com.appsmith.external.datatypes.ClientDataType; +import com.appsmith.external.dtos.ExecuteActionDTO; +import com.appsmith.external.dtos.ParamProperty; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.Param; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.repositories.NewActionRepository; +import com.appsmith.server.services.AnalyticsService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.AuthenticationValidator; +import com.appsmith.server.services.DatasourceContextService; +import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.NewActionService; +import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.PluginService; +import com.appsmith.server.services.SessionUserService; +import com.appsmith.server.services.ce.NewActionServiceCE; +import com.appsmith.server.solutions.ActionPermission; +import com.appsmith.server.solutions.DatasourcePermission; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.FormHttpMessageReader; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; +import org.springframework.http.codec.multipart.MultipartHttpMessageReader; +import org.springframework.http.codec.multipart.Part; +import org.springframework.http.codec.xml.Jaxb2XmlDecoder; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.reactive.function.BodyExtractor; +import org.springframework.web.reactive.function.BodyExtractors; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +class ActionExecutionSolutionCEImplTest { + + ActionExecutionSolutionCEImpl actionExecutionSolution; + + @SpyBean + NewActionService newActionService; + @MockBean + ActionPermission actionPermission; + @MockBean + ObservationRegistry observationRegistry; + @SpyBean + ObjectMapper objectMapper; + @MockBean + NewActionRepository repository; + @MockBean + DatasourceService datasourceService; + @MockBean + PluginService pluginService; + @MockBean + DatasourceContextService datasourceContextService; + @MockBean + PluginExecutorHelper pluginExecutorHelper; + @MockBean + NewPageService newPageService; + @MockBean + ApplicationService applicationService; + @MockBean + SessionUserService sessionUserService; + @MockBean + AuthenticationValidator authenticationValidator; + @MockBean + DatasourcePermission datasourcePermission; + @MockBean + AnalyticsService analyticsService; + + private BodyExtractor.Context context; + + private Map hints; + + @BeforeEach + public void beforeEach() { + actionExecutionSolution = new ActionExecutionSolutionCEImpl( + newActionService, + actionPermission, + observationRegistry, + objectMapper, + repository, + datasourceService, + pluginService, + datasourceContextService, + pluginExecutorHelper, + newPageService, + applicationService, + sessionUserService, + authenticationValidator, + datasourcePermission, + analyticsService + ); + + ObservationRegistry.ObservationConfig mockObservationConfig = Mockito.mock(ObservationRegistry.ObservationConfig.class); + Mockito.when(observationRegistry.observationConfig()).thenReturn(mockObservationConfig); + } + + @BeforeEach + public void createContext() { + final List> messageReaders = new ArrayList<>(); + messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); + messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes())); + messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); + messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); + messageReaders.add(new FormHttpMessageReader()); + DefaultPartHttpMessageReader partReader = new DefaultPartHttpMessageReader(); + messageReaders.add(partReader); + messageReaders.add(new MultipartHttpMessageReader(partReader)); + + this.context = new BodyExtractor.Context() { + @Override + public List> messageReaders() { + return messageReaders; + } + + @Override + public Optional serverResponse() { + return Optional.empty(); + } + + @Override + public Map hints() { + return hints; + } + }; + this.hints = new HashMap<>(); + } + + + @Test + public void testExecuteAction_withoutExecuteActionDTOPart_failsValidation() { + final Mono actionExecutionResultMono = actionExecutionSolution.executeAction(Flux.empty(), null, null); + + StepVerifier + .create(actionExecutionResultMono) + .expectErrorMatches(e -> e instanceof AppsmithException && + e.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.ACTION_ID))) + .verify(); + } + + @Test + public void testExecuteAction_withMalformedExecuteActionDTO_failsValidation() { + MockServerHttpRequest mock = MockServerHttpRequest + .method(HttpMethod.POST, URI.create("https://example.com")) + .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) + .body(""" + --boundary\r + Content-Disposition: form-data; name="executeActionDTO"\r + \r + irrelevant content\r + --boundary--\r + """); + + final Flux partsFlux = BodyExtractors.toParts() + .extract(mock, this.context); + + final Mono actionExecutionResultMono = actionExecutionSolution.executeAction(partsFlux, null, null); + + StepVerifier + .create(actionExecutionResultMono) + .expectErrorMatches(e -> e instanceof AppsmithException && + e.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage("executeActionDTO"))) + .verify(); + } + + @Test + public void testExecuteAction_withoutActionId_failsValidation() { + MockServerHttpRequest mock = MockServerHttpRequest + .method(HttpMethod.POST, URI.create("https://example.com")) + .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) + .body(""" + --boundary\r + Content-Disposition: form-data; name="executeActionDTO"\r + \r + {"viewMode":false}\r + --boundary--\r + """); + + final Flux partsFlux = BodyExtractors.toParts() + .extract(mock, this.context); + + final Mono actionExecutionResultMono = actionExecutionSolution.executeAction(partsFlux, null, null); + + StepVerifier + .create(actionExecutionResultMono) + .expectErrorMatches(e -> e instanceof AppsmithException && + e.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.ACTION_ID))) + .verify(); + } + + + @Test + public void testExecuteAPIWithUsualOrderingOfTheParts() { + String usualOrderOfParts = """ + --boundary\r + Content-Disposition: form-data; name="executeActionDTO"\r + \r + {"actionId":"63285a3388e48972c7519b18","viewMode":false,"paramProperties":{"k0":{"datatype": "string"}}}\r + --boundary\r + Content-Disposition: form-data; name="parameterMap"\r + \r + {"Input1.text":"k0"}\r + --boundary\r + Content-Disposition: form-data; name="k0"; filename="blob"\r + Content-Type: text/plain\r + \r + xyz\r + --boundary--"""; + + MockServerHttpRequest mock = MockServerHttpRequest + .method(HttpMethod.POST, URI.create("https://example.com")) + .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) + .body(usualOrderOfParts); + + final Flux partsFlux = BodyExtractors.toParts() + .extract(mock, this.context); + + ActionExecutionSolutionCE executionSolutionSpy = spy(actionExecutionSolution); + + Mono actionExecutionResultMono = executionSolutionSpy.executeAction(partsFlux, null, null); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("test body"); + mockResult.setTitle("test title"); + + NewAction newAction = new NewAction(); + newAction.setId("63285a3388e48972c7519b18"); + doReturn(Mono.just(mockResult)).when(executionSolutionSpy).executeAction(any(), any()); + doReturn(Mono.just(newAction)).when(newActionService).findByBranchNameAndDefaultActionId(any(), any(), any()); + + + StepVerifier + .create(actionExecutionResultMono) + .assertNext(response -> { + assertNotNull(response); + assertTrue(response.getIsExecutionSuccess()); + assertEquals(mockResult.getBody().toString(), response.getBody().toString()); + }) + .verifyComplete(); + } + + @Test + public void testExecuteAPIWithParameterMapAsLastPart() { + String parameterMapAtLast = """ + --boundary\r + Content-Disposition: form-data; name="executeActionDTO"\r + \r + {"actionId":"63285a3388e48972c7519b18","viewMode":false,"paramProperties":{"k0":{"datatype": "string"}}}\r + --boundary\r + Content-Disposition: form-data; name="k0"; filename="blob"\r + Content-Type: text/plain\r + \r + xyz\r + --boundary\r + Content-Disposition: form-data; name="parameterMap"\r + \r + {"Input1.text":"k0"}\r + --boundary--"""; + + MockServerHttpRequest mock = MockServerHttpRequest + .method(HttpMethod.POST, URI.create("https://example.com")) + .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) + .body(parameterMapAtLast); + + final Flux partsFlux = BodyExtractors.toParts() + .extract(mock, this.context); + + ActionExecutionSolutionCE executionSolutionSpy = spy(actionExecutionSolution); + + Mono actionExecutionResultMono = executionSolutionSpy.executeAction(partsFlux, null, null); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("test body"); + mockResult.setTitle("test title"); + + NewAction newAction = new NewAction(); + newAction.setId("63285a3388e48972c7519b18"); + doReturn(Mono.just(mockResult)).when(executionSolutionSpy).executeAction(any(), any()); + doReturn(Mono.just(newAction)).when(newActionService).findByBranchNameAndDefaultActionId(any(), any(), any()); + + + StepVerifier + .create(actionExecutionResultMono) + .assertNext(response -> { + assertNotNull(response); + assertTrue(response.getIsExecutionSuccess()); + assertEquals(mockResult.getBody().toString(), response.getBody().toString()); + }) + .verifyComplete(); + } + + @Test + public void testParsePartsAndGetParamsFlux_withBlobIdentifiers_replacesValueInParam() { + String partsWithBlobRefs = """ + --boundary\r + Content-Disposition: form-data; name="executeActionDTO"\r + \r + {"actionId":"63285a3388e48972c7519b18","viewMode":false,"paramProperties":{"k0":{"datatype": "string", "blobIdentifiers": ["blob:12345678-1234-1234-1234-123456781234"]}}}\r + --boundary\r + Content-Disposition: form-data; name="parameterMap"\r + \r + {"Input1.text":"k0"}\r + --boundary\r + Content-Disposition: form-data; name="k0"; filename="blob"\r + Content-Type: text/plain\r + \r + {"name": "randomName", "data": "blob:12345678-1234-1234-1234-123456781234"}\r + --boundary\r + Content-Disposition: form-data; name="blob:12345678-1234-1234-1234-123456781234"; filename="blob"\r + Content-Type: text/plain\r + \r + xy\\nz\r + --boundary--"""; + + MockServerHttpRequest mock = MockServerHttpRequest + .method(HttpMethod.POST, URI.create("https://example.com")) + .contentType(new MediaType("multipart", "form-data", Map.of("boundary", "boundary"))) + .body(partsWithBlobRefs); + + final Flux partsFlux = BodyExtractors.toParts() + .extract(mock, this.context); + + AtomicLong atomicLong = new AtomicLong(); + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + Mono> paramsListMono = actionExecutionSolution.parsePartsAndGetParamsFlux(partsFlux, atomicLong, executeActionDTO).collectList().cache(); + + StepVerifier.create(paramsListMono) + .assertNext(paramsList -> { + assertEquals(1, paramsList.size()); + Param param = paramsList.get(0); + assertEquals("{\"name\": \"randomName\", \"data\": \"blob:12345678-1234-1234-1234-123456781234\"}", param.getValue()); + }) + .verifyComplete(); + } + + @Test + public void testEnrichExecutionParams_withBlobReference_performsSubstitutionCorrectly() { + AtomicLong atomicLong = new AtomicLong(45L); + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId("testId"); + executeActionDTO.setViewMode(false); + executeActionDTO.setParamProperties(Map.of("k0", new ParamProperty("string", List.of("blobId")))); + executeActionDTO.setParameterMap(Map.of("Input1.text", "k0")); + executeActionDTO.setBlobValuesMap(Map.of("blobId", "xy\\nz")); + Param param1 = new Param(); + param1.setValue("{\"name\": \"randomName\", \"data\": \"blobId\"}"); + param1.setPseudoBindingName("k0"); + List params = List.of(param1); + + Mono enrichedDto = actionExecutionSolution.enrichExecutionParam(atomicLong, executeActionDTO, params); + + StepVerifier.create(enrichedDto) + .assertNext(dto -> { + assertEquals(45, dto.getTotalReadableByteCount()); + + Param param = dto.getParams().get(0); + assertEquals(ClientDataType.STRING, param.getClientDataType()); + assertEquals("{\"name\": \"randomName\", \"data\": \"xy\\\\nz\"}", param.getValue()); + }) + .verifyComplete(); + } + +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCETest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCETest.java new file mode 100644 index 0000000000..c9e734e8df --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCETest.java @@ -0,0 +1,1838 @@ +package com.appsmith.server.solutions.ce; + +import com.appsmith.external.constants.DisplayDataType; +import com.appsmith.external.dtos.ExecuteActionDTO; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionDTO; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.Datasource; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.PaginationField; +import com.appsmith.external.models.PaginationType; +import com.appsmith.external.models.ParsedDataType; +import com.appsmith.external.models.Property; +import com.appsmith.external.models.WidgetSuggestionDTO; +import com.appsmith.external.models.WidgetType; +import com.appsmith.external.plugins.PluginExecutor; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.GitApplicationMetadata; +import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.Plugin; +import com.appsmith.server.domains.User; +import com.appsmith.server.domains.Workspace; +import com.appsmith.server.dtos.MockDataSource; +import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.helpers.MockPluginExecutor; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.helpers.WidgetSuggestionHelper; +import com.appsmith.server.repositories.DatasourceRepository; +import com.appsmith.server.repositories.PermissionGroupRepository; +import com.appsmith.server.repositories.PluginRepository; +import com.appsmith.server.services.ApplicationPageService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.AstService; +import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.LayoutActionService; +import com.appsmith.server.services.LayoutService; +import com.appsmith.server.services.MockDataService; +import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.PermissionGroupService; +import com.appsmith.server.services.PluginService; +import com.appsmith.server.services.UserService; +import com.appsmith.server.services.WorkspaceService; +import com.appsmith.server.solutions.ActionExecutionSolution; +import com.appsmith.server.solutions.ImportExportApplicationService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.minidev.json.JSONArray; +import net.minidev.json.JSONObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.http.HttpMethod; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.appsmith.server.acl.AclPermission.READ_PAGES; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +public class ActionExecutionSolutionCETest { + + @Autowired + ActionExecutionSolution actionExecutionSolution; + + @Autowired + ApplicationPageService applicationPageService; + + @Autowired + NewPageService newPageService; + + @Autowired + UserService userService; + + @Autowired + WorkspaceService workspaceService; + + @Autowired + PluginRepository pluginRepository; + + @MockBean + PluginExecutorHelper pluginExecutorHelper; + + @MockBean + PluginExecutor pluginExecutor; + + @Autowired + ObjectMapper objectMapper; + + @Autowired + LayoutActionService layoutActionService; + + @Autowired + LayoutService layoutService; + + @Autowired + DatasourceService datasourceService; + + @Autowired + ImportExportApplicationService importExportApplicationService; + + @SpyBean + PluginService pluginService; + + @Autowired + ApplicationService applicationService; + + @Autowired + MockDataService mockDataService; + + @Autowired + PermissionGroupRepository permissionGroupRepository; + + @Autowired + PermissionGroupService permissionGroupService; + + @SpyBean + AstService astService; + @Autowired + DatasourceRepository datasourceRepository; + + Application testApp = null; + + PageDTO testPage = null; + + Application gitConnectedApp = null; + + PageDTO gitConnectedPage = null; + + Datasource datasource; + + String workspaceId; + + String branchName; + + @BeforeEach + @WithUserDetails(value = "api_user") + public void setup() { + + User apiUser = userService.findByEmail("api_user").block(); + + Workspace toCreate = new Workspace(); + toCreate.setName("ActionServiceCE_Test"); + + if (workspaceId == null) { + Workspace workspace = workspaceService.create(toCreate, apiUser, Boolean.FALSE).block(); + workspaceId = workspace.getId(); + } + + if (testApp == null && testPage == null) { + //Create application and page which will be used by the tests to create actions for. + Application application = new Application(); + application.setName(UUID.randomUUID().toString()); + testApp = applicationPageService.createApplication(application, workspaceId).block(); + + final String pageId = testApp.getPages().get(0).getId(); + + testPage = newPageService.findPageById(pageId, READ_PAGES, false).block(); + + Layout layout = testPage.getLayouts().get(0); + JSONObject dsl = new JSONObject(Map.of("text", "{{ query1.data }}")); + + JSONObject dsl2 = new JSONObject(); + dsl2.put("widgetName", "Table1"); + dsl2.put("type", "TABLE_WIDGET"); + Map primaryColumns = new HashMap<>(); + JSONObject jsonObject = new JSONObject(Map.of("key", "value")); + primaryColumns.put("_id", "{{ query1.data }}"); + primaryColumns.put("_class", jsonObject); + dsl2.put("primaryColumns", primaryColumns); + final ArrayList objects = new ArrayList<>(); + JSONArray temp2 = new JSONArray(); + temp2.addAll(List.of(new JSONObject(Map.of("key", "primaryColumns._id")))); + dsl2.put("dynamicBindingPathList", temp2); + objects.add(dsl2); + dsl.put("children", objects); + + layout.setDsl(dsl); + layout.setPublishedDsl(dsl); + } + + if (gitConnectedApp == null) { + Application newApp = new Application(); + newApp.setName(UUID.randomUUID().toString()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("actionServiceTest"); + newApp.setGitApplicationMetadata(gitData); + gitConnectedApp = applicationPageService.createApplication(newApp, workspaceId) + .flatMap(application -> { + application.getGitApplicationMetadata().setDefaultApplicationId(application.getId()); + return applicationService.save(application) + .zipWhen(application1 -> importExportApplicationService.exportApplicationById(application1.getId(), gitData.getBranchName())); + }) + // Assign the branchName to all the resources connected to the application + .flatMap(tuple -> importExportApplicationService.importApplicationInWorkspace(workspaceId, tuple.getT2(), tuple.getT1().getId(), gitData.getBranchName())) + .block(); + + gitConnectedPage = newPageService.findPageById(gitConnectedApp.getPages().get(0).getId(), READ_PAGES, false).block(); + + branchName = gitConnectedApp.getGitApplicationMetadata().getBranchName(); + } + + datasource = new Datasource(); + datasource.setName("Default Database"); + datasource.setWorkspaceId(workspaceId); + Plugin installed_plugin = pluginRepository.findByPackageName("restapi-plugin").block(); + datasource.setPluginId(installed_plugin.getId()); + datasource.setDatasourceConfiguration(new DatasourceConfiguration()); + } + + @AfterEach + @WithUserDetails(value = "api_user") + public void cleanup() { + applicationPageService.deleteApplication(testApp.getId()).block(); + testApp = null; + testPage = null; + + } + + private void executeAndAssertAction(ExecuteActionDTO executeActionDTO, ActionConfiguration actionConfiguration, + ActionExecutionResult mockResult, List expectedReturnDataTypes) { + + List expectedWidgets = mockResult.getSuggestedWidgets(); + Mono actionExecutionResultMono = executeAction(executeActionDTO, actionConfiguration, mockResult); + + StepVerifier.create(actionExecutionResultMono) + .assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.getBody()).isEqualTo(mockResult.getBody()); + assertThat(result.getDataTypes()).hasToString(expectedReturnDataTypes.toString()); + assertThat(result.getSuggestedWidgets()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrderElementsOf(expectedWidgets); + assertThat(result.getRequest().getActionId()).isEqualTo(executeActionDTO.getActionId()); + assertThat(result.getRequest().getRequestedAt()).isBefore(Instant.now()); + }) + .verifyComplete(); + } + + private Mono executeAction(ExecuteActionDTO executeActionDTO, ActionConfiguration actionConfiguration, ActionExecutionResult mockResult) { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.just(mockResult)); + Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); + + Mono actionExecutionResultMono = actionExecutionSolution.executeAction(executeActionDTO, null); + return actionExecutionResultMono; + } + + @Test + @WithUserDetails(value = "api_user") + public void testVariableSubstitution() { + String json = "{\n" + + " \n" + + " \"deleted\": false,\n" + + " \"config\": {\n" + + " \"CONTAINER_WIDGET\": [\n" + + " {\n" + + " \"_id\": \"7\",\n" + + " \"sectionName\": \"General\",\n" + + " \"children\": [\n" + + " {\n" + + " \"_id\": \"7.1\",\n" + + " \"helpText\": \"Use a html color name, HEX, RGB or RGBA value\",\n" + + " \"placeholderText\": \"#FFFFFF / Gray / rgb(255, 99, 71)\",\n" + + " \"propertyName\": \"backgroundColor\",\n" + + " \"label\": \"Background Color\",\n" + + " \"controlType\": \"INPUT_TEXT\"\n" + + " },\n" + + " {\n" + + " \"_id\": \"7.2\",\n" + + " \"helpText\": \"Controls the visibility of the widget\",\n" + + " \"propertyName\": \"isVisible\",\n" + + " \"label\": \"Visible\",\n" + + " \"controlType\": \"SWITCH\",\n" + + " \"isJSConvertible\": true\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"name\": \"propertyPane\"\n" + + "}"; + + ActionDTO action = new ActionDTO(); + action.setActionConfiguration(new ActionConfiguration()); + action.getActionConfiguration().setBody("{{Input.text}}"); + + ActionDTO renderedAction = actionExecutionSolution.variableSubstitution(action, Map.of("Input.text", json)); + assertThat(renderedAction).isNotNull(); + assertThat(renderedAction.getActionConfiguration().getBody()).isEqualTo(json); + } + + @Test + @WithUserDetails(value = "api_user") + public void testVariableSubstitutionWithNewline() { + ActionDTO action = new ActionDTO(); + action.setActionConfiguration(new ActionConfiguration()); + action.getActionConfiguration().setBody("{{Input.text}}"); + + ActionDTO renderedAction = actionExecutionSolution.variableSubstitution(action, Map.of("Input.text", "name\nvalue")); + assertThat(renderedAction).isNotNull(); + assertThat(renderedAction.getActionConfiguration().getBody()).isEqualTo("name\nvalue"); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecute() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("response-body"); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, List.of(new ParsedDataType(DisplayDataType.RAW))); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteNullRequestBody() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("response-body"); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setName("testActionExecuteNullRequestBody"); + action.setPageId(testPage.getId()); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, List.of(new ParsedDataType(DisplayDataType.RAW))); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteDbQuery() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("response-body"); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select * from users"); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecuteDbQuery"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteErrorResponse() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("response-body"); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHeaders(List.of( + new Property("random-header-key", "random-header-value"), + new Property("", "") + )); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecuteErrorResponse"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + AppsmithPluginException pluginException = new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR); + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.error(pluginException)); + Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); + + Mono executionResultMono = actionExecutionSolution.executeAction(executeActionDTO, null); + + StepVerifier.create(executionResultMono) + .assertNext(result -> { + assertThat(result.getIsExecutionSuccess()).isFalse(); + assertThat(result.getStatusCode()).isEqualTo(pluginException.getAppErrorCode().toString()); + assertThat(result.getTitle()).isEqualTo(pluginException.getTitle()); + assertThat(result.getRequest().getActionId()).isEqualTo(createdAction.getId()); + assertThat(result.getRequest().getRequestedAt()).isBefore(Instant.now()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteNullPaginationParameters() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("response-body"); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHeaders(List.of( + new Property("random-header-key", "random-header-value"), + new Property("", "") + )); + actionConfiguration.setPaginationType(PaginationType.URL); + actionConfiguration.setNext(null); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecuteErrorResponse"); + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasource.setDatasourceConfiguration(datasourceConfiguration); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + executeActionDTO.setPaginationField(PaginationField.NEXT); + + AppsmithPluginException pluginException = new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR); + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.error(pluginException)); + Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); + + Mono executionResultMono = actionExecutionSolution.executeAction(executeActionDTO, null); + + StepVerifier.create(executionResultMono) + .assertNext(result -> { + assertThat(result.getIsExecutionSuccess()).isFalse(); + assertThat(result.getStatusCode()).isEqualTo(pluginException.getAppErrorCode().toString()); + assertThat(result.getTitle()).isEqualTo(pluginException.getTitle()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteSecondaryStaleConnection() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("response-body"); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHeaders(List.of( + new Property("random-header-key", "random-header-value"), + new Property("", "") + )); + actionConfiguration.setTimeoutInMillisecond(String.valueOf(1000)); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecuteSecondaryStaleConnection"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(Mono.error(new StaleConnectionException())).thenReturn(Mono.error(new StaleConnectionException())); + Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); + + Mono executionResultMono = actionExecutionSolution.executeAction(executeActionDTO, null); + + StepVerifier.create(executionResultMono) + .assertNext(result -> { + assertThat(result.getIsExecutionSuccess()).isFalse(); + assertThat(result.getStatusCode()).isEqualTo(AppsmithPluginError.STALE_CONNECTION_ERROR.getAppErrorCode()); + assertThat(result.getTitle()).isEqualTo(AppsmithPluginError.STALE_CONNECTION_ERROR.getTitle()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteTimeout() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("response-body"); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHeaders(List.of( + new Property("random-header-key", "random-header-value"), + new Property("", "") + )); + actionConfiguration.setTimeoutInMillisecond(String.valueOf(10)); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecuteTimeout"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .thenAnswer(x -> Mono.delay(Duration.ofMillis(1000)).ofType(ActionExecutionResult.class)); + Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); + + Mono executionResultMono = actionExecutionSolution.executeAction(executeActionDTO, null); + + StepVerifier.create(executionResultMono) + .assertNext(result -> { + assertThat(result.getIsExecutionSuccess()).isFalse(); + assertThat(result.getStatusCode()).isEqualTo(AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR.getAppErrorCode().toString()); + assertThat(result.getTitle()).isEqualTo(AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR.getTitle()); + }) + .verifyComplete(); + } + + + + @Test + @WithUserDetails(value = "api_user") + public void checkRecoveryFromStaleConnections() { + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("response-body"); + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.executeParameterizedWithMetrics(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .thenThrow(new StaleConnectionException()) + .thenReturn(Mono.just(mockResult)); + Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select * from users"); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("checkRecoveryFromStaleConnections"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + Mono actionExecutionResultMono = actionExecutionSolution.executeAction(executeActionDTO, null); + + StepVerifier.create(actionExecutionResultMono) + .assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.getBody()).isEqualTo(mockResult.getBody()); + }) + .verifyComplete(); + } + + + @Test + @WithUserDetails(value = "api_user") + public void executeActionWithExternalDatasource() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + Mockito.when(pluginService.getEditorConfigLabelMap(Mockito.anyString())).thenReturn(Mono.just(new HashMap<>())); + + Datasource externalDatasource = new Datasource(); + externalDatasource.setName("Default Database"); + externalDatasource.setWorkspaceId(workspaceId); + Plugin restApiPlugin = pluginRepository.findByPackageName("restapi-plugin").block(); + externalDatasource.setPluginId(restApiPlugin.getId()); + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setUrl("some url here"); + externalDatasource.setDatasourceConfiguration(datasourceConfiguration); + Datasource savedDs = datasourceService.create(externalDatasource).block(); + + ActionDTO action = new ActionDTO(); + action.setName("actionWithExternalDatasource"); + action.setPageId(testPage.getId()); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(savedDs); + + + Mono resultMono = layoutActionService.createSingleAction(action, Boolean.FALSE) + .flatMap(savedAction -> { + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(savedAction.getId()); + executeActionDTO.setViewMode(false); + return actionExecutionSolution.executeAction(executeActionDTO, null); + }); + + + StepVerifier + .create(resultMono) + .assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.getStatusCode()).isEqualTo("200"); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteWithTableReturnType() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("[\n" + + "{\"name\": \"Richard\", \"profession\": \"medical\"},\n" + + "{\"name\": \"John\", \"profession\": \"self employed\"},\n" + + "{\"name\": \"Mary\", \"profession\": \"engineer\"}\n" + + "]"); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.TABLE), new ParsedDataType(DisplayDataType.JSON) + , new ParsedDataType(DisplayDataType.RAW))); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteWithJsonReturnType() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("{\n" + + " \"name\":\"John\",\n" + + " \"age\":30,\n" + + " \"cars\": {\n" + + " \"car1\":\"Ford\",\n" + + " \"car2\":\"BMW\",\n" + + " \"car3\":\"Fiat\"\n" + + " }\n" + + " }"); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.JSON), new ParsedDataType(DisplayDataType.RAW))); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteWithPreAssignedReturnType() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("{\n" + + " \"name\":\"John\",\n" + + " \"age\":30,\n" + + " \"cars\": {\n" + + " \"car1\":\"Ford\",\n" + + " \"car2\":\"BMW\",\n" + + " \"car3\":\"Fiat\"\n" + + " }\n" + + " }"); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + } + + @Test + @WithUserDetails(value = "api_user") + public void testActionExecuteReturnTypeWithNullResultBody() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(null); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + + List widgetTypeList = new ArrayList<>(); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, new ArrayList<>()); + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithChartWidgetData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": [\n" + + " {\n" + + " \"x\": \"Mon\",\n" + + " \"y\": 10000\n" + + " },\n" + + " {\n" + + " \"x\": \"Tue\",\n" + + " \"y\": 12000\n" + + " },\n" + + " {\n" + + " \"x\": \"Wed\",\n" + + " \"y\": 32000\n" + + " },\n" + + " {\n" + + " \"x\": \"Thu\",\n" + + " \"y\": 28000\n" + + " },\n" + + " {\n" + + " \"x\": \"Fri\",\n" + + " \"y\": 14000\n" + + " },\n" + + " {\n" + + " \"x\": \"Sat\",\n" + + " \"y\": 19000\n" + + " },\n" + + " {\n" + + " \"x\": \"Sun\",\n" + + " \"y\": 36000\n" + + " }\n" + + "]}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "x", "y")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "x", "x")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithTableWidgetData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": [\n" + + "\t{\n" + + "\t\t\"id\": \"0001\",\n" + + "\t\t\"type\": \"donut\",\n" + + "\t\t\"name\": \"Cake\",\n" + + "\t\t\"ppu\": 0.55,\n" + + "\t\t\"batters\":\n" + + "\t\t\t{\n" + + "\t\t\t\t\"batter\":\n" + + "\t\t\t\t\t[\n" + + "\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" },\n" + + "\t\t\t\t\t\t{ \"id\": \"1002\", \"type\": \"Chocolate\" },\n" + + "\t\t\t\t\t\t{ \"id\": \"1003\", \"type\": \"Blueberry\" },\n" + + "\t\t\t\t\t\t{ \"id\": \"1004\", \"type\": \"Devil's Food\" }\n" + + "\t\t\t\t\t]\n" + + "\t\t\t},\n" + + "\t\t\"topping\":\n" + + "\t\t\t[\n" + + "\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" + + "\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" + + "\t\t\t\t{ \"id\": \"5005\", \"type\": \"Sugar\" },\n" + + "\t\t\t\t{ \"id\": \"5007\", \"type\": \"Powdered Sugar\" },\n" + + "\t\t\t\t{ \"id\": \"5006\", \"type\": \"Chocolate with Sprinkles\" },\n" + + "\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" + + "\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" + + "\t\t\t]\n" + + "\t},\n" + + "\t{\n" + + "\t\t\"id\": \"0002\",\n" + + "\t\t\"type\": \"donut\",\n" + + "\t\t\"name\": \"Raised\",\n" + + "\t\t\"ppu\": 0.55,\n" + + "\t\t\"batters\":\n" + + "\t\t\t{\n" + + "\t\t\t\t\"batter\":\n" + + "\t\t\t\t\t[\n" + + "\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" }\n" + + "\t\t\t\t\t]\n" + + "\t\t\t},\n" + + "\t\t\"topping\":\n" + + "\t\t\t[\n" + + "\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" + + "\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" + + "\t\t\t\t{ \"id\": \"5005\", \"type\": \"Sugar\" },\n" + + "\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" + + "\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" + + "\t\t\t]\n" + + "\t},\n" + + "\t{\n" + + "\t\t\"id\": \"0003\",\n" + + "\t\t\"type\": \"donut\",\n" + + "\t\t\"name\": \"Old Fashioned\",\n" + + "\t\t\"ppu\": 0.55,\n" + + "\t\t\"batters\":\n" + + "\t\t\t{\n" + + "\t\t\t\t\"batter\":\n" + + "\t\t\t\t\t[\n" + + "\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" },\n" + + "\t\t\t\t\t\t{ \"id\": \"1002\", \"type\": \"Chocolate\" }\n" + + "\t\t\t\t\t]\n" + + "\t\t\t},\n" + + "\t\t\"topping\":\n" + + "\t\t\t[\n" + + "\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" + + "\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" + + "\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" + + "\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" + + "\t\t\t]\n" + + "\t}\n" + + "]}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "id", "ppu")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "id", "type")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithListWidgetData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": [\n" + + " {\n" + + " \"url\": \"images/thumbnails/0001.jpg\",\n" + + " \"width\": 32,\n" + + " \"height\": 32\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0001.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0002.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0002.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0003.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0004.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0005.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0006.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0007.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0008.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0009.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " },\n" + + " {\n" + + " \"url\": \"images/0010.jpg\",\n" + + " \"width\": 200,\n" + + " \"height\": 200\n" + + " }\n" + + "]}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithDropdownWidgetData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": [\n" + + " {\n" + + " \"CarType\": \"BMW\",\n" + + " \"carID\": \"bmw123\"\n" + + " },\n" + + " {\n" + + " \"CarType\": \"mercedes\",\n" + + " \"carID\": \"merc123\"\n" + + " },\n" + + " {\n" + + " \"CarType\": \"volvo\",\n" + + " \"carID\": \"vol123r\"\n" + + " },\n" + + " {\n" + + " \"CarType\": \"ford\",\n" + + " \"carID\": \"ford123\"\n" + + " }\n" + + " ]}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "CarType", "carID")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithArrayOfStringsDropDownWidget() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\":[\"string1\", \"string2\", \"string3\", \"string4\"] }"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.INPUT_WIDGET)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithArrayOfArray() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\":[[\"string1\", \"string2\", \"string3\", \"string4\"]," + + "[\"string5\", \"string6\", \"string7\", \"string8\"]," + + "[\"string9\", \"string10\", \"string11\", \"string12\"]] }"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithEmptyData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\":[] }"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithNumericData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": [1] }"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.INPUT_WIDGET)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithJsonNodeData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{\"data\": {\n" + + " \"next\": \"https://mock-api.appsmith.com/users?page=2&pageSize=10\",\n" + + " \"previous\": null,\n" + + " \"users\": [\n" + + " {\n" + + " \"id\": 3,\n" + + " \"name\": \"Demetre\",\n" + + " \"status\": \"APPROVED\",\n" + + " \"gender\": \"Male\",\n" + + " \"avatar\": \"https://robohash.org/iustooptiocum.jpg?size=100x100&set=set1\",\n" + + " \"email\": \"aaaa@bbb.com\",\n" + + " \"address\": \"262 Saint Paul Park\",\n" + + " \"createdAt\": \"2020-05-01T17:30:50.000Z\",\n" + + " \"updatedAt\": \"2019-10-08T14:55:53.000Z\"\n" + + " },\n" + + " {\n" + + " \"id\": 4,\n" + + " \"name\": \"Currey\",\n" + + " \"status\": \"APPROVED\",\n" + + " \"gender\": \"Female\",\n" + + " \"avatar\": \"https://robohash.org/aspernaturnatusrepellat.jpg?size=100x100&set=set1\",\n" + + " \"email\": \"cbrayson3@taobao.com\",\n" + + " \"address\": \"35180 Lotheville Street!\",\n" + + " \"createdAt\": \"2019-12-30T03:54:23.000Z\",\n" + + " \"updatedAt\": \"2020-08-12T17:43:01.016Z\"\n" + + " }\n" + + " ]\n" + + "}}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, "users")); + widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.CHART_WIDGET, "users", "name", "id")); + widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, "users")); + widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.SELECT_WIDGET, "users", "name", "status")); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithJsonObjectData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": {\n" + + " \"id\": 1,\n" + + " \"name\": \"Barty Crouch\",\n" + + " \"status\": \"APPROVED\",\n" + + " \"gender\": \"\",\n" + + " \"avatar\": \"https://robohash.org/sednecessitatibuset.png?size=100x100&set=set1\",\n" + + " \"email\": \"barty.crouch@gmail.com\",\n" + + " \"address\": \"St Petersberg #911 4th main\",\n" + + " \"createdAt\": \"2020-03-16T18:00:05.000Z\",\n" + + " \"updatedAt\": \"2020-08-12T17:29:31.980Z\"\n" + + " } }"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionAfterExecutionWithJsonArrayObjectData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{ \"data\": {\n" + + " \"id\": 1,\n" + + " \"name\": \"Barty Crouch\",\n" + + " \"status\": \"APPROVED\",\n" + + " \"gender\": \"\",\n" + + " \"avatar\": \"https://robohash.org/sednecessitatibuset.png?size=100x100&set=set1\",\n" + + " \"email\": \"barty.crouch@gmail.com\",\n" + + " \"address\": \"St Petersberg #911 4th main\",\n" + + " \"createdAt\": \"2020-03-16T18:00:05.000Z\",\n" + + " \"updatedAt\": \"2020-08-12T17:29:31.980Z\"\n" + + " }," + + "\"data\": {\n" + + " \"id\": 2,\n" + + " \"name\": \"Jenelle Kibbys\",\n" + + " \"status\": \"APPROVED\",\n" + + " \"gender\": \"Female\",\n" + + " \"avatar\": \"https://robohash.org/quiaasperiorespariatur.bmp?size=100x100&set=set1\",\n" + + " \"email\": \"jkibby1@hp.com\",\n" + + " \"address\": \"85 Tennessee Plaza\",\n" + + " \"createdAt\": \"2019-10-04T03:22:23.000Z\",\n" + + " \"updatedAt\": \"2019-09-11T20:18:38.000Z\"\n" + + " } }"; + final JsonNode arrNode = new ObjectMapper().readTree(data); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionNestedData() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{\"data\": {\n" + + " \"next\": \"https://mock-api.appsmith.com/users?page=2&pageSize=10\",\n" + + " \"previous\": null,\n" + + " \"users\": [1, 2, 3]\n" + + "}}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, "users")); + widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, "users")); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void testWidgetSuggestionNestedDataEmpty() throws JsonProcessingException { + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + final String data = "{\"data\": {\n" + + " \"next\": \"https://mock-api.appsmith.com/users?page=2&pageSize=10\",\n" + + " \"previous\": null,\n" + + " \"users\": []\n" + + "}}"; + final JsonNode arrNode = new ObjectMapper().readTree(data).get("data"); + ; + + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(arrNode); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, null)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + @Test + @WithUserDetails(value = "api_user") + public void suggestWidget_ArrayListData_SuggestTableTextChartDropDownWidget() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + ArrayList listData = new ArrayList<>(); + JSONObject jsonObject = new JSONObject(Map.of("url", "images/thumbnails/0001.jpg", "width", 32, "height", 32)); + listData.add(jsonObject); + jsonObject = new JSONObject(Map.of("url", "images/0001.jpg", "width", 42, "height", 22)); + listData.add(jsonObject); + jsonObject = new JSONObject(Map.of("url", "images/0002.jpg", "width", 52, "height", 12)); + listData.add(jsonObject); + jsonObject = new JSONObject(Map.of("url", "images/0003.jpg", "width", 62, "height", 52)); + listData.add(jsonObject); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(listData); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + } + + @Test + @WithUserDetails(value = "api_user") + public void suggestWidget_ArrayListData_SuggestTableTextDropDownWidget() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + ArrayList listData = new ArrayList<>(); + JSONObject jsonObject = new JSONObject(Map.of("url", "images/thumbnails/0001.jpg", "width", "32", "height", "32")); + listData.add(jsonObject); + jsonObject = new JSONObject(Map.of("url", "images/0001.jpg", "width", "42", "height", "22")); + listData.add(jsonObject); + jsonObject = new JSONObject(Map.of("url", "images/0002.jpg", "width", "52", "height", "12")); + listData.add(jsonObject); + jsonObject = new JSONObject(Map.of("url", "images/0003.jpg", "width", "62", "height", "52")); + listData.add(jsonObject); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(listData); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "width", "url")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + } + + @Test + @WithUserDetails(value = "api_user") + public void suggestWidget_ArrayListDataEmpty_SuggestTextWidget() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + ActionExecutionResult mockResult = new ActionExecutionResult(); + ArrayList listData = new ArrayList<>(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody(listData); + mockResult.setStatusCode("200"); + mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); + mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.POST); + actionConfiguration.setBody("random-request-body"); + actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + + } + + + + @Test + @WithUserDetails(value = "api_user") + public void executeAction_actionOnMockDatasource_success() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + Mockito.when(pluginService.getEditorConfigLabelMap(Mockito.anyString())).thenReturn(Mono.just(new HashMap<>())); + Mockito.when(pluginExecutor.getHintMessages(Mockito.any(), Mockito.any())) + .thenReturn(Mono.zip(Mono.just(new HashSet<>()), Mono.just(new HashSet<>()))); + + ActionExecutionResult mockResult = new ActionExecutionResult(); + mockResult.setIsExecutionSuccess(true); + mockResult.setBody("response-body"); + + Plugin installed_plugin = pluginRepository.findByPackageName("restapi-plugin").block(); + MockDataSource mockDataSource = new MockDataSource(); + mockDataSource.setName("Users"); + mockDataSource.setWorkspaceId(workspaceId); + mockDataSource.setPackageName("postgres-plugin"); + mockDataSource.setPluginId(installed_plugin.getId()); + Datasource mockDatasource = mockDataService.createMockDataSet(mockDataSource).block(); + + List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + mockResult.setSuggestedWidgets(widgetTypeList); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select * from users"); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecuteDbQuery"); + action.setDatasource(mockDatasource); + ActionDTO createdAction = layoutActionService.createSingleAction(action, Boolean.FALSE).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, + List.of(new ParsedDataType(DisplayDataType.RAW))); + } +}