diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index 614cc90eb1..7a34cafe5d 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -166,11 +166,15 @@ class ActionAPI extends API { } static executeAction( - executeAction: ExecuteActionRequest, + executeAction: FormData, timeout?: number, ): AxiosPromise { return API.post(ActionAPI.url + "/execute", executeAction, undefined, { timeout: timeout || DEFAULT_EXECUTE_ACTION_TIMEOUT_MS, + headers: { + accept: "application/json", + "Content-Type": "multipart/form-data", + }, }); } diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index dc9772630f..eaf7fed07a 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -17,7 +17,6 @@ import ActionAPI, { ActionResponse, ExecuteActionRequest, PaginationField, - Property, } from "api/ActionAPI"; import { getAction, @@ -62,7 +61,7 @@ import { EMPTY_RESPONSE } from "components/editorComponents/ApiResponseView"; import { AppState } from "reducers"; import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "@appsmith/constants/ApiConstants"; import { evaluateActionBindings } from "sagas/EvaluationsSaga"; -import { isBlobUrl, mapToPropList, parseBlobUrl } from "utils/AppsmithUtils"; +import { isBlobUrl, parseBlobUrl } from "utils/AppsmithUtils"; import { getType, Types } from "utils/TypeHelpers"; import { matchPath } from "react-router"; import { @@ -209,6 +208,7 @@ function* readBlob(blobUrl: string): any { */ function* evaluateActionParams( bindings: string[] | undefined, + formData: FormData, executionParams?: Record | string, ) { if (_.isNil(bindings) || bindings.length === 0) return []; @@ -220,8 +220,7 @@ function* evaluateActionParams( executionParams, ); - // Convert to object and transform non string values - const actionParams: Record = {}; + // Add keys values to formData for the multipart submission for (let i = 0; i < bindings.length; i++) { const key = bindings[i]; let value = values[i]; @@ -229,9 +228,9 @@ function* evaluateActionParams( if (isBlobUrl(value)) { value = yield call(readBlob, value); } - actionParams[key] = value; + + formData.append(encodeURIComponent(key), value); } - return mapToPropList(actionParams); } export default function* executePluginActionTriggerSaga( @@ -735,18 +734,11 @@ function* executePluginActionSaga( ); yield put(executePluginActionRequest({ id: actionId })); - const actionParams: Property[] = yield call( - evaluateActionParams, - pluginAction.jsonPathKeys, - params, - ); - const appMode = yield select(getAppMode); const timeout = yield select(getActionTimeout, actionId); const executeActionRequest: ExecuteActionRequest = { actionId: actionId, - params: actionParams, viewMode: appMode === APP_MODE.PUBLISHED, }; @@ -754,8 +746,12 @@ function* executePluginActionSaga( executeActionRequest.paginationField = paginationField; } + const formData = new FormData(); + formData.append("executeActionDTO", JSON.stringify(executeActionRequest)); + yield call(evaluateActionParams, pluginAction.jsonPathKeys, formData, params); + const response: ActionExecutionResponse = yield ActionAPI.executeAction( - executeActionRequest, + formData, timeout, ); PerformanceTracker.stopAsyncTracking( diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/helpers/DataUtils.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/helpers/DataUtils.java index a1ed20bf28..031148e351 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/helpers/DataUtils.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/helpers/DataUtils.java @@ -4,7 +4,6 @@ import com.appsmith.external.dtos.MultipartFormDataDTO; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.models.Property; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.JsonSyntaxException; @@ -17,12 +16,14 @@ import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.MediaType; import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -72,7 +73,7 @@ public class DataUtils { case MediaType.MULTIPART_FORM_DATA_VALUE: return parseMultipartFileData((List) body); default: - return BodyInserters.fromValue(body); + return BodyInserters.fromValue(((String) body).getBytes(StandardCharsets.UTF_8)); } } @@ -156,11 +157,18 @@ public class DataUtils { final MultipartFormDataType multipartFormDataType = MultipartFormDataType.valueOf(property.getType().toUpperCase(Locale.ROOT)); if (MultipartFormDataType.TEXT.equals(multipartFormDataType)) { - bodyBuilder.part(key, property.getValue()); + byte[] valueBytesArray = new byte[0]; + if (StringUtils.hasLength(String.valueOf(property.getValue()))) { + valueBytesArray = String.valueOf(property.getValue()).getBytes(StandardCharsets.ISO_8859_1); + } + bodyBuilder.part( + key, + valueBytesArray, + MediaType.TEXT_PLAIN); } else if (MultipartFormDataType.FILE.equals(multipartFormDataType)) { try { populateFileTypeBodyBuilder(bodyBuilder, property, outputMessage); - } catch (JsonProcessingException e) { + } catch (IOException e) { e.printStackTrace(); throw new AppsmithPluginException( AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, @@ -179,22 +187,23 @@ public class DataUtils { } private void populateFileTypeBodyBuilder(MultipartBodyBuilder bodyBuilder, Property property, ClientHttpRequest outputMessage) - throws JsonProcessingException { - final Object fileValue = property.getValue(); + throws IOException { + final String fileValue = (String) property.getValue(); final String key = property.getKey(); List multipartFormDataDTOs = new ArrayList<>(); - if (String.valueOf(fileValue).startsWith("{")) { + if (fileValue.startsWith("{")) { // Check whether the JSON string is an object - final MultipartFormDataDTO multipartFormDataDTO = objectMapper.readValue(String.valueOf(fileValue), + final MultipartFormDataDTO multipartFormDataDTO = objectMapper.readValue( + fileValue, MultipartFormDataDTO.class); multipartFormDataDTOs.add(multipartFormDataDTO); - } else if (String.valueOf(fileValue).startsWith("[")) { + } else if (fileValue.startsWith("[")) { // Check whether the JSON string is an array multipartFormDataDTOs = Arrays.asList( objectMapper.readValue( - String.valueOf(fileValue), + (String) (fileValue), MultipartFormDataDTO[].class)); } else { throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, @@ -204,9 +213,8 @@ public class DataUtils { multipartFormDataDTOs.forEach(multipartFormDataDTO -> { final MultipartFormDataDTO finalMultipartFormDataDTO = multipartFormDataDTO; Flux data = DataBufferUtils.readInputStream( - () -> new ByteArrayInputStream(String - .valueOf(finalMultipartFormDataDTO.getData()) - .getBytes(StandardCharsets.UTF_8)), + () -> new ByteArrayInputStream(String.valueOf(finalMultipartFormDataDTO.getData()) + .getBytes(StandardCharsets.ISO_8859_1)), outputMessage.bufferFactory(), 4096); diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index 57e67f74de..ba13d5fc45 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -479,6 +479,7 @@ public class RestApiPlugin extends BasePlugin { String encode = Base64.encode(body); result.setBody(encode); responseDataType = ResponseDataType.IMAGE; + } else if (binaryDataTypes.contains(contentType.toString())) { String encode = Base64.encode(body); result.setBody(encode); diff --git a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/helpers/DataUtilsTest.java b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/helpers/DataUtilsTest.java index f97a354fc8..9dda6b45b8 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/helpers/DataUtilsTest.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/helpers/DataUtilsTest.java @@ -4,6 +4,7 @@ import com.appsmith.external.models.Property; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.core.codec.ByteArrayEncoder; import org.springframework.core.codec.ByteBufferEncoder; import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.DataBufferEncoder; @@ -48,6 +49,7 @@ public class DataUtilsTest { public void createContext() { final List> messageWriters = new ArrayList<>(); messageWriters.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); + messageWriters.add(new EncoderHttpMessageWriter<>(new ByteArrayEncoder())); messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); messageWriters.add(new ResourceHttpMessageWriter()); Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(); @@ -91,7 +93,7 @@ public class DataUtilsTest { Mono result = bodyInserter.insert(request, this.context); StepVerifier.create(result).expectComplete().verify(); StepVerifier.create(request.getBodyAsString()) - .expectNext("\"\"") + .expectNext("") .expectComplete() .verify(); } @@ -123,9 +125,9 @@ public class DataUtilsTest { "Content-Length: 8\r\n" + "\r\n" + "textData")); + Assert.assertTrue(content.contains("Content-Type: text/plain")); Assert.assertTrue(content.contains( "Content-Disposition: form-data; name=\"textType\"\r\n" + - "Content-Type: text/plain;charset=UTF-8\r\n" + "Content-Length: 8\r\n" + "\r\n" + "textData")); 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 b6c9ac6be8..ac4a34d71f 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 @@ -5,6 +5,7 @@ import com.appsmith.server.controllers.ce.ActionControllerCE; import com.appsmith.server.services.ActionCollectionService; import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.NewActionService; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; 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 b2425a62f9..70ae85d406 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 @@ -1,6 +1,5 @@ package com.appsmith.server.controllers.ce; -import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.server.constants.FieldName; import com.appsmith.server.constants.Url; @@ -16,6 +15,8 @@ import com.appsmith.server.services.NewActionService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.codec.multipart.Part; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -28,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.validation.Valid; @@ -70,10 +72,10 @@ public class ActionControllerCE { .map(updatedResource -> new ResponseDTO<>(HttpStatus.OK.value(), updatedResource, null)); } - @PostMapping("/execute") - public Mono> executeAction(@RequestBody ExecuteActionDTO executeActionDTO, + @PostMapping(value = "/execute", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public Mono> executeAction(@RequestBody Flux partFlux, @RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName) { - return newActionService.executeAction(executeActionDTO, branchName) + return newActionService.executeAction(partFlux, branchName) .map(updatedResource -> new ResponseDTO<>(HttpStatus.OK.value(), updatedResource, null)); } @@ -119,7 +121,7 @@ public class ActionControllerCE { /** * This function fetches all actions in edit mode. * To fetch the actions in view mode, check the function `getActionsForViewMode` - * + *

* The controller function is primarily used with param applicationId by the client to fetch the actions in edit * mode. * 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 287f083dbf..a82be3854d 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 @@ -10,6 +10,7 @@ import com.appsmith.server.dtos.ActionViewDTO; import com.appsmith.server.dtos.LayoutActionUpdateDTO; import com.appsmith.server.services.CrudService; import org.springframework.data.domain.Sort; +import org.springframework.http.codec.multipart.Part; import org.springframework.util.MultiValueMap; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -36,10 +37,10 @@ public interface NewActionServiceCE extends CrudService { Mono executeAction(ExecuteActionDTO executeActionDTO); + Mono executeAction(Flux partsFlux, String branchName); + Mono getValidActionForExecution(ExecuteActionDTO executeActionDTO, String actionId, NewAction newAction); - Mono executeAction(ExecuteActionDTO executeActionDTO, String branchName); - T variableSubstitution(T configuration, Map replaceParamsMap); Mono findByUnpublishedNameAndPageId(String name, String pageId, 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 587ffec530..465cc4118f 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 @@ -60,9 +60,11 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang3.ObjectUtils; import org.bson.types.ObjectId; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.http.codec.multipart.Part; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.LinkedMultiValueMap; @@ -75,6 +77,9 @@ import reactor.util.function.Tuple2; import javax.lang.model.SourceVersion; import javax.validation.Validator; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -529,7 +534,7 @@ public class NewActionServiceCEImpl extends BaseService params = executeActionDTO.getParams(); 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 + // 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.isEmpty(param.getKey()) && param.getValue() == null) { param.setValue(""); @@ -748,6 +753,67 @@ public class NewActionServiceCEImpl extends BaseService addDataTypesAndSetSuggestedWidget(result, executeActionDTO.getViewMode())); } + @Override + public Mono executeAction(Flux partFlux, String branchName) { + + final ExecuteActionDTO dto = new ExecuteActionDTO(); + return partFlux + .flatMap(part -> { + final String key = part.name(); + if ("executeActionDTO".equals(key)) { + 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) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "executeActionDTO")); + } + }) + .flatMap(executeActionDTO -> { + dto.setActionId(executeActionDTO.getActionId()); + dto.setPaginationField(executeActionDTO.getPaginationField()); + dto.setViewMode(executeActionDTO.getViewMode()); + + return Mono.empty(); + }); + } + return Mono.just(part); + }) + .flatMap(part -> { + final Param param = new Param(); + param.setKey(URLDecoder.decode(part.name(), StandardCharsets.UTF_8)); + return DataBufferUtils + .join(part.content()) + .map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); + param.setValue(new String(bytes, StandardCharsets.UTF_8)); + return param; + }); + }) + .collectList() + .flatMap(params -> { + if(dto.getActionId() == null) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ACTION_ID)); + } + dto.setParams(params); + return Mono.just(dto); + }) + .flatMap(executeActionDTO -> this + .findByBranchNameAndDefaultActionId(branchName, executeActionDTO.getActionId(), EXECUTE_ACTIONS) + .map(branchedAction -> { + executeActionDTO.setActionId(branchedAction.getId()); + return executeActionDTO; + }) + ) + .flatMap(this::executeAction); + } + @Override public Mono getValidActionForExecution(ExecuteActionDTO executeActionDTO, String actionId, NewAction newAction) { Mono actionDTOMono = Mono.just(newAction) @@ -783,15 +849,6 @@ public class NewActionServiceCEImpl extends BaseService executeAction(ExecuteActionDTO executeActionDTO, String branchName){ - - return this.findByBranchNameAndDefaultActionId(branchName, executeActionDTO.getActionId(), EXECUTE_ACTIONS) - .flatMap(branchedAction -> { - executeActionDTO.setActionId(branchedAction.getId()); - return executeAction(executeActionDTO); - }); - } /* * - Get label for request params. * - Transform request params list: [""] to a map: {"label": {"value": ...}} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/PageLoadActionsUtilCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/PageLoadActionsUtilCEImpl.java index c67eeddcd4..a045f08205 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/PageLoadActionsUtilCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/PageLoadActionsUtilCEImpl.java @@ -789,7 +789,7 @@ public class PageLoadActionsUtilCEImpl implements PageLoadActionsUtilCE { for (Property x : dynamicBindingPathList) { final String fieldPath = String.valueOf(x.getKey()); - // Ignore pagination configuration since paginatio technically does not belong to dynamic binding list. + // Ignore pagination configuration since pagination technically does not belong to dynamic binding list. if (fieldPath.equals("prev") || fieldPath.equals("next")) { continue; } @@ -823,7 +823,7 @@ public class PageLoadActionsUtilCEImpl implements PageLoadActionsUtilCE { } // After updating the parent, check for the types if (parent == null) { - // path doesnt seem to exist. Ignore. + // path doesn't seem to exist. Ignore. } else if (parent instanceof String) { // If we get String value, then this is a leaf node isLeafNode = true; diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceImplTest.java new file mode 100644 index 0000000000..4cafd1a290 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/NewActionServiceImplTest.java @@ -0,0 +1,220 @@ +package com.appsmith.server.services.ce; + +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.server.acl.PolicyGenerator; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.helpers.PolicyUtils; +import com.appsmith.server.helpers.ResponseUtils; +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.ConfigService; +import com.appsmith.server.services.DatasourceContextService; +import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.MarketplaceService; +import com.appsmith.server.services.NewActionService; +import com.appsmith.server.services.NewActionServiceImpl; +import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.PluginService; +import com.appsmith.server.services.SessionUserService; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoConverter; +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.junit4.SpringRunner; +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.core.scheduler.Scheduler; +import reactor.test.StepVerifier; + +import javax.validation.Validator; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + + +@RunWith(SpringRunner.class) +@Slf4j +public class NewActionServiceImplTest { + + NewActionService newActionService; + + @MockBean + Scheduler scheduler; + @MockBean + Validator validator; + @MockBean + MongoConverter mongoConverter; + @MockBean + ReactiveMongoTemplate reactiveMongoTemplate; + @MockBean + AnalyticsService analyticsService; + @MockBean + DatasourceService datasourceService; + @MockBean + PluginService pluginService; + @MockBean + DatasourceContextService datasourceContextService; + @MockBean + PluginExecutorHelper pluginExecutorHelper; + @MockBean + MarketplaceService marketplaceService; + @MockBean + PolicyGenerator policyGenerator; + @MockBean + NewPageService newPageService; + @MockBean + ApplicationService applicationService; + @MockBean + SessionUserService sessionUserService; + @MockBean + PolicyUtils policyUtils; + @MockBean + AuthenticationValidator authenticationValidator; + @MockBean + ConfigService configService; + @MockBean + ResponseUtils responseUtils; + + @MockBean + NewActionRepository newActionRepository; + + private BodyExtractor.Context context; + + private Map hints; + + @Before + public void setup() { + newActionService = new NewActionServiceImpl(scheduler, + validator, + mongoConverter, + reactiveMongoTemplate, + newActionRepository, + analyticsService, + datasourceService, + pluginService, + datasourceContextService, + pluginExecutorHelper, + marketplaceService, + policyGenerator, + newPageService, + applicationService, + sessionUserService, + policyUtils, + authenticationValidator, + configService, + responseUtils + ); + } + + @Before + public void createContext() { + final List> messageReaders = new ArrayList<>(); + messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); + messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); + 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); + + 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\n" + + "Content-Disposition: form-data; name=\"executeActionDTO\"\r\n" + "\r\n" + "irrelevant content\r\n" + + "--boundary--\r\n"); + + final Flux partsFlux = BodyExtractors.toParts() + .extract(mock, this.context); + + final Mono actionExecutionResultMono = newActionService.executeAction(partsFlux, 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\n" + + "Content-Disposition: form-data; name=\"executeActionDTO\"\r\n" + "\r\n" + "{\"viewMode\":false}\r\n" + + "--boundary--\r\n"); + + final Flux partsFlux = BodyExtractors.toParts() + .extract(mock, this.context); + + final Mono actionExecutionResultMono = newActionService.executeAction(partsFlux, null); + + StepVerifier + .create(actionExecutionResultMono) + .expectErrorMatches(e -> e instanceof AppsmithException && + e.getMessage().equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.ACTION_ID))) + .verify(); + } + +} \ No newline at end of file