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.
This commit is contained in:
Nidhi 2023-05-15 13:16:59 +05:30 committed by GitHub
parent 44d1837678
commit 1337134cd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 3450 additions and 2878 deletions

View File

@ -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;

View File

@ -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);
}

View File

@ -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<ResponseDTO<ActionExecutionResult>> executeAction(@RequestBody Flux<Part> 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));
}

View File

@ -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);
}

View File

@ -37,14 +37,6 @@ public interface NewActionServiceCE extends CrudService<NewAction, String> {
Mono<ActionDTO> updateUnpublishedAction(String id, ActionDTO action);
Mono<ActionExecutionResult> executeAction(ExecuteActionDTO executeActionDTO, String environmentName);
Mono<ActionExecutionResult> executeAction(Flux<Part> partsFlux, String branchName, String environmentName);
Mono<ActionDTO> getValidActionForExecution(ExecuteActionDTO executeActionDTO, String actionId, NewAction newAction);
<T> T variableSubstitution(T configuration, Map<String, String> replaceParamsMap);
Mono<ActionDTO> findByUnpublishedNameAndPageId(String name, String pageId, AclPermission permission);
Mono<ActionDTO> findActionDTObyIdAndViewMode(String id, Boolean viewMode, AclPermission permission);

View File

@ -0,0 +1,6 @@
package com.appsmith.server.solutions;
import com.appsmith.server.solutions.ce.ActionExecutionSolutionCE;
public interface ActionExecutionSolution extends ActionExecutionSolutionCE {
}

View File

@ -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);
}
}

View File

@ -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<ActionExecutionResult> executeAction(Flux<Part> partFlux, String branchName, String environmentName);
Mono<ActionExecutionResult> executeAction(ExecuteActionDTO executeActionDTO, String environmentName);
Mono<ActionDTO> getValidActionForExecution(ExecuteActionDTO executeActionDTO, String actionId, NewAction newAction);
<T> T variableSubstitution(T configuration, Map<String, String> replaceParamsMap);
}

View File

@ -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<String, Object> 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<HttpMessageReader<?>> 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<HttpMessageReader<?>> messageReaders() {
return messageReaders;
}
@Override
public Optional<ServerHttpResponse> serverResponse() {
return Optional.empty();
}
@Override
public Map<String, Object> hints() {
return hints;
}
};
this.hints = new HashMap<>();
}
@Test
public void testExecuteAction_withoutExecuteActionDTOPart_failsValidation() {
final Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
final Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
final Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
NewActionServiceCE newActionServiceSpy = spy(newActionService);
Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
NewActionServiceCE newActionServiceSpy = spy(newActionService);
Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
AtomicLong atomicLong = new AtomicLong();
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
Mono<List<Param>> 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<Param> params = List.of(param1);
Mono<ExecuteActionDTO> 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();
}
}

View File

@ -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<String, Object> 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<HttpMessageReader<?>> 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<HttpMessageReader<?>> messageReaders() {
return messageReaders;
}
@Override
public Optional<ServerHttpResponse> serverResponse() {
return Optional.empty();
}
@Override
public Map<String, Object> hints() {
return hints;
}
};
this.hints = new HashMap<>();
}
@Test
public void testExecuteAction_withoutExecuteActionDTOPart_failsValidation() {
final Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
final Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
final Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
ActionExecutionSolutionCE executionSolutionSpy = spy(actionExecutionSolution);
Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
ActionExecutionSolutionCE executionSolutionSpy = spy(actionExecutionSolution);
Mono<ActionExecutionResult> 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<Part> partsFlux = BodyExtractors.toParts()
.extract(mock, this.context);
AtomicLong atomicLong = new AtomicLong();
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
Mono<List<Param>> 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<Param> params = List.of(param1);
Mono<ExecuteActionDTO> 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();
}
}