diff --git a/app/server/appsmith-server/pom.xml b/app/server/appsmith-server/pom.xml index 3099d3a79d..03ca21c6ce 100644 --- a/app/server/appsmith-server/pom.xml +++ b/app/server/appsmith-server/pom.xml @@ -218,6 +218,11 @@ junit-jupiter-engine test + + org.awaitility + awaitility + test + org.junit.platform diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java index 0b1609797e..050f13f918 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java @@ -22,6 +22,7 @@ import org.springframework.data.convert.SimpleTypeInformationMapper; import org.springframework.data.convert.TypeInformationMapper; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; +import org.springframework.data.mongodb.ReactiveMongoTransactionManager; import org.springframework.data.mongodb.config.EnableReactiveMongoAuditing; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; @@ -32,6 +33,8 @@ import org.springframework.data.mongodb.core.convert.MongoTypeMapper; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.reactive.TransactionalOperator; import java.util.Arrays; import java.util.Collections; @@ -125,4 +128,13 @@ public class MongoConfig { return new EncryptionMongoEventListener(encryptionService); } + @Bean + public ReactiveTransactionManager reactiveTransactionManager(ReactiveMongoDatabaseFactory factory) { + return new ReactiveMongoTransactionManager(factory); + } + + @Bean + public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) { + return TransactionalOperator.create(transactionManager); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCE.java index 9c75b9c58b..94d5e079af 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCE.java @@ -68,4 +68,6 @@ public interface CustomApplicationRepositoryCE extends AppsmithRepository updateFieldByDefaultIdAndBranchName(String defaultId, String defaultIdPath, Map fieldNameValueMap, String branchName, String branchNamePath, AclPermission permission); + + Mono findByNameAndWorkspaceId(String applicationName, String workspaceId, AclPermission permission); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCEImpl.java index 81e93a553c..6c0c091e33 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/CustomApplicationRepositoryCEImpl.java @@ -275,4 +275,11 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp return super.updateFieldByDefaultIdAndBranchName(defaultId, defaultIdPath, fieldValueMap, branchName, branchNamePath, permission); } + + @Override + public Mono findByNameAndWorkspaceId(String applicationName, String workspaceId, AclPermission permission) { + Criteria workspaceIdCriteria = where(fieldName(QApplication.application.workspaceId)).is(workspaceId); + Criteria applicationNameCriteria = where(fieldName(QApplication.application.name)).is(applicationName); + return queryOne(List.of(workspaceIdCriteria, applicationNameCriteria), permission); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/GitServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/GitServiceImpl.java index 880e63f6d8..1712d53616 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/GitServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/GitServiceImpl.java @@ -14,7 +14,6 @@ import com.appsmith.server.solutions.ApplicationPermission; import com.appsmith.server.solutions.DatasourcePermission; import com.appsmith.server.solutions.ImportExportApplicationService; import com.appsmith.server.solutions.PagePermission; -import io.sentry.protocol.App; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java index d290edb877..8ec15474c9 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationPageServiceCEImpl.java @@ -53,7 +53,6 @@ import jakarta.annotation.Nullable; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.bson.types.ObjectId; -import org.springframework.dao.DuplicateKeyException; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -71,6 +70,7 @@ import java.util.Set; import java.util.stream.Collectors; import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties; +import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; import static org.apache.commons.lang.ObjectUtils.defaultIfNull; @@ -1175,22 +1175,25 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE { Mono userMono = sessionUserService.getCurrentUser().cache(); Mono applicationWithPoliciesMono = this.setApplicationPolicies(userMono, application.getWorkspaceId(), application); + Mono applicationMono = applicationService.findByNameAndWorkspaceId(actualName, application.getWorkspaceId(), MANAGE_APPLICATIONS); - return applicationWithPoliciesMono - .zipWith(userMono) - .flatMap(tuple -> { - Application application1 = tuple.getT1(); - application1.setModifiedBy(tuple.getT2().getUsername()); // setting modified by to current user - // We can't use create or createApplication method here as we are expecting update operation if the - // _id is available with application object - return applicationService.save(application); - }) - .onErrorResume(DuplicateKeyException.class, error -> { - if (error.getMessage() != null) { - return this.createOrUpdateSuffixedApplication(application, name, 1 + suffix); - } - throw error; - }); + // We are taking pessimistic approach as this flow is used in import application where we are using transactions + // which creates problem if we hit duplicate key exception + return applicationMono + .flatMap(application1 -> + this.createOrUpdateSuffixedApplication(application, name, 1 + suffix) + ) + .switchIfEmpty(Mono.defer(() -> + applicationWithPoliciesMono + .zipWith(userMono) + .flatMap(tuple -> { + Application application1 = tuple.getT1(); + application1.setModifiedBy(tuple.getT2().getUsername()); // setting modified by to current user + // We can't use create or createApplication method here as we are expecting update operation if the + // _id is available with application object + return applicationService.save(application); + }) + )); } /** diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationServiceCE.java index 5fe1c5dfb1..3972c0a8e6 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationServiceCE.java @@ -78,7 +78,6 @@ public interface ApplicationServiceCE extends CrudService { String defaultApplicationId, String fieldName, AclPermission aclPermission); - Mono findBranchedApplicationId(String branchName, String defaultApplicationId, AclPermission permission); Flux findAllApplicationsByDefaultApplicationId(String defaultApplicationId, AclPermission permission); @@ -99,4 +98,5 @@ public interface ApplicationServiceCE extends CrudService { public Mono deleteAppNavigationLogo(String branchName, String applicationId); + Mono findByNameAndWorkspaceId(String applicationName, String workspaceId, AclPermission permission); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationServiceCEImpl.java index b1561e09e8..1beb380817 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/ApplicationServiceCEImpl.java @@ -813,7 +813,7 @@ public class ApplicationServiceCEImpl extends BaseService updateMono = this.update(applicationId, branchedApplication, branchName); - if (!StringUtils.hasLength(oldAssetId)){ + if (!StringUtils.hasLength(oldAssetId)) { return updateMono; } else { return assetService.remove(oldAssetId).then(updateMono); @@ -822,7 +822,12 @@ public class ApplicationServiceCEImpl extends BaseService findByNameAndWorkspaceId(String applicationName, String workspaceId, AclPermission permission) { + return repository.findByNameAndWorkspaceId(applicationName, workspaceId, permission); } @Override diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImpl.java index 611f794fdf..d2a85c74aa 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImpl.java @@ -22,6 +22,7 @@ import com.google.gson.Gson; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; +import org.springframework.transaction.reactive.TransactionalOperator; @Slf4j @Component @@ -51,12 +52,13 @@ public class ImportExportApplicationServiceImpl extends ImportExportApplicationS ApplicationPermission applicationPermission, PagePermission pagePermission, ActionPermission actionPermission, - Gson gson) { + Gson gson, + TransactionalOperator transactionalOperator) { super(datasourceService, sessionUserService, newActionRepository, datasourceRepository, pluginRepository, workspaceService, applicationService, newPageService, applicationPageService, newPageRepository, newActionService, sequenceService, examplesWorkspaceCloner, actionCollectionRepository, actionCollectionService, themeService, analyticsService, customJSLibService, datasourcePermission, - workspacePermission, applicationPermission, pagePermission, actionPermission, gson); + workspacePermission, applicationPermission, pagePermission, actionPermission, gson, transactionalOperator); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImplV2.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImplV2.java index 48b968e8d6..0ea598ca09 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImplV2.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ImportExportApplicationServiceImplV2.java @@ -22,6 +22,8 @@ import com.google.gson.Gson; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionalOperator; @Slf4j @Component @@ -51,12 +53,13 @@ public class ImportExportApplicationServiceImplV2 extends ImportExportApplicatio ApplicationPermission applicationPermission, PagePermission pagePermission, ActionPermission actionPermission, - Gson gson) { + Gson gson, + TransactionalOperator transactionalOperator) { super(datasourceService, sessionUserService, newActionRepository, datasourceRepository, pluginRepository, workspaceService, applicationService, newPageService, applicationPageService, newPageRepository, newActionService, sequenceService, examplesWorkspaceCloner, actionCollectionRepository, actionCollectionService, themeService, analyticsService, customJSLibService, datasourcePermission, - workspacePermission, applicationPermission, pagePermission, actionPermission, gson); + workspacePermission, applicationPermission, pagePermission, actionPermission, gson, transactionalOperator); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java index 5f2be404b0..5d51b71d3f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImpl.java @@ -23,6 +23,7 @@ import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.CustomJSLib; import com.appsmith.server.domains.GitApplicationMetadata; +import com.appsmith.server.domains.GitApplicationMetadata; import com.appsmith.server.domains.Layout; import com.appsmith.server.domains.NewAction; import com.appsmith.server.domains.NewPage; @@ -78,6 +79,7 @@ import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.Part; +import org.springframework.transaction.reactive.TransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -133,6 +135,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica private final PagePermission pagePermission; private final ActionPermission actionPermission; private final Gson gson; + private final TransactionalOperator transactionalOperator; /** * This function will give the application resource to rebuild the application in import application flow @@ -1202,12 +1205,9 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica }) .onErrorResume(throwable -> { log.error("Error while importing the application ", throwable.getMessage()); - if (importedApplication.getId() != null && applicationId == null) { - return applicationPageService.deleteApplication(importedApplication.getId()) - .then(Mono.error(new AppsmithException(AppsmithError.GENERIC_JSON_IMPORT_ERROR, workspaceId, throwable.getMessage()))); - } - return Mono.error(new AppsmithException(AppsmithError.UNKNOWN_PLUGIN_REFERENCE)); - }); + return Mono.error(new AppsmithException(AppsmithError.GENERIC_JSON_IMPORT_ERROR, workspaceId, "")); + }) + .as(transactionalOperator::transactional); // Import Application is currently a slow API because it needs to import and create application, pages, actions // and action collection. This process may take time and the client may cancel the request. This leads to the flow diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImplV2.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImplV2.java index 68193a6691..02895291a0 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImplV2.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ImportExportApplicationServiceCEImplV2.java @@ -78,6 +78,8 @@ import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.Part; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -132,6 +134,7 @@ public class ImportExportApplicationServiceCEImplV2 implements ImportExportAppli private final PagePermission pagePermission; private final ActionPermission actionPermission; private final Gson gson; + private final TransactionalOperator transactionalOperator; private static final Set ALLOWED_CONTENT_TYPES = Set.of(MediaType.APPLICATION_JSON); private static final String INVALID_JSON_FILE = "invalid json file"; @@ -1231,12 +1234,9 @@ public class ImportExportApplicationServiceCEImplV2 implements ImportExportAppli }) .onErrorResume(throwable -> { log.error("Error while importing the application ", throwable.getMessage()); - if (importedApplication.getId() != null && applicationId == null) { - return applicationPageService.deleteApplication(importedApplication.getId()) - .then(Mono.error(new AppsmithException(AppsmithError.GENERIC_JSON_IMPORT_ERROR, workspaceId, throwable.getMessage()))); - } return Mono.error(new AppsmithException(AppsmithError.GENERIC_JSON_IMPORT_ERROR, workspaceId, "")); - }); + }) + .as(transactionalOperator::transactional); // Import Application is currently a slow API because it needs to import and create application, pages, actions // and action collection. This process may take time and the client may cancel the request. This leads to the flow diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/TransactionalConfig.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/TransactionalConfig.java new file mode 100644 index 0000000000..f7505a3366 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/TransactionalConfig.java @@ -0,0 +1,17 @@ +package com.appsmith.server.configurations; + +import de.flapdoodle.embed.mongo.commands.MongodArguments; +import de.flapdoodle.embed.mongo.config.Storage; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TransactionalConfig { + + @Bean + MongodArguments mongodArguments() { + return MongodArguments.builder() + .replication(Storage.of("appsmith-replica-set", 10)) + .build(); + } +} diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java index e17371e4a3..fc4f7c5026 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ImportExportApplicationServiceTests.java @@ -2901,14 +2901,13 @@ public class ImportExportApplicationServiceTests { assert appJson != null; final String randomId = UUID.randomUUID().toString(); appJson.getDatasourceList().get(0).setPluginId(randomId); - final Mono resultMono = workspaceService - .create(newWorkspace) - .flatMap(workspace -> importExportApplicationService.importApplicationInWorkspace(workspace.getId(), appJson)); + Workspace createdWorkspace = workspaceService.create(newWorkspace).block(); + final Mono resultMono = importExportApplicationService.importApplicationInWorkspace(createdWorkspace.getId(), appJson); StepVerifier .create(resultMono) .expectErrorMatches(throwable -> throwable instanceof AppsmithException && - throwable.getMessage().equals(AppsmithError.UNKNOWN_PLUGIN_REFERENCE.getMessage(randomId))) + throwable.getMessage().equals(AppsmithError.GENERIC_JSON_IMPORT_ERROR.getMessage(createdWorkspace.getId(), ""))) .verify(); } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/transactions/ImportApplicationTransactionServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/transactions/ImportApplicationTransactionServiceTest.java new file mode 100644 index 0000000000..47a4fce365 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/transactions/ImportApplicationTransactionServiceTest.java @@ -0,0 +1,162 @@ +package com.appsmith.server.transactions; + +import com.appsmith.server.domains.ActionCollection; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.domains.Workspace; +import com.appsmith.server.dtos.ApplicationJson; +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.migrations.JsonSchemaMigration; +import com.appsmith.server.repositories.ActionCollectionRepository; +import com.appsmith.server.repositories.NewActionRepository; +import com.appsmith.server.services.ActionCollectionService; +import com.appsmith.server.services.NewActionService; +import com.appsmith.server.services.WorkspaceService; +import com.appsmith.server.solutions.ImportExportApplicationService; +import com.google.gson.Gson; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.autoconfigure.data.mongo.AutoConfigureDataMongo; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.http.MediaType; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +// All the test case are for failure or exception. Test cases for valid json file is already present in ImportExportApplicationServiceTest class + +@AutoConfigureDataMongo +@SpringBootTest( + properties = "de.flapdoodle.mongodb.embedded.version=5.0.5" +) +@EnableAutoConfiguration() +@TestPropertySource(properties = "property=C") +@DirtiesContext +public class ImportApplicationTransactionServiceTest { + + @Autowired + @Qualifier("importExportServiceCEImplV2") + ImportExportApplicationService importExportApplicationService; + + @Autowired + WorkspaceService workspaceService; + + @Autowired + MongoTemplate mongoTemplate; + + @MockBean + NewActionService newActionService; + + @MockBean + NewActionRepository newActionRepository; + + @MockBean + ActionCollectionService actionCollectionService; + + @MockBean + ActionCollectionRepository actionCollectionRepository; + + @MockBean + PluginExecutorHelper pluginExecutorHelper; + + private ApplicationJson applicationJson = new ApplicationJson(); + + Long applicationCount = 0L, pageCount = 0L, actionCount = 0L, actionCollectionCount = 0L; + + @BeforeEach + public void setup() { + Mockito + .when(pluginExecutorHelper.getPluginExecutor(Mockito.any())) + .thenReturn(Mono.just(new MockPluginExecutor())); + + applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json").block(); + applicationCount = mongoTemplate.count(new Query(), Application.class); + pageCount = mongoTemplate.count(new Query(), NewPage.class); + actionCount = mongoTemplate.count(new Query(), NewAction.class); + actionCollectionCount = mongoTemplate.count(new Query(), ActionCollection.class); + } + + + private FilePart createFilePart(String filePath) { + FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); + Flux dataBufferFlux = DataBufferUtils + .read( + new ClassPathResource(filePath), + new DefaultDataBufferFactory(), + 4096) + .cache(); + + Mockito.when(filepart.content()).thenReturn(dataBufferFlux); + Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.APPLICATION_JSON); + + return filepart; + + } + + private Mono createAppJson(String filePath) { + FilePart filePart = createFilePart(filePath); + + Mono stringifiedFile = DataBufferUtils.join(filePart.content()) + .map(dataBuffer -> { + byte[] data = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(data); + DataBufferUtils.release(dataBuffer); + return new String(data); + }); + + return stringifiedFile + .map(data -> { + Gson gson = new Gson(); + return gson.fromJson(data, ApplicationJson.class); + }) + .map(JsonSchemaMigration::migrateApplicationToLatestSchema); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_exceptionDuringActionSave_savedPagesAndApplicationReverted() { + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Template Workspace"); + + Mockito.when(newActionService.save(Mockito.any())) + .thenThrow(new AppsmithException(AppsmithError.GENERIC_BAD_REQUEST)); + + Workspace createdWorkspace = workspaceService.create(newWorkspace).block(); + + Mono resultMono = importExportApplicationService.importApplicationInWorkspace(createdWorkspace.getId(), applicationJson); + + // Check if expected exception is thrown + StepVerifier + .create(resultMono) + .expectErrorMatches(error -> error instanceof AppsmithException && error.getMessage().equals(AppsmithError.GENERIC_JSON_IMPORT_ERROR.getMessage(createdWorkspace.getId(), ""))) + .verify(); + + // After the import application failed in the middle of execution after the application and pages are saved to DB + // check if the saved pages reverted after the exception + assertThat(mongoTemplate.count(new Query(), Application.class)).isEqualTo(applicationCount); + assertThat(mongoTemplate.count(new Query(), NewPage.class)).isEqualTo(pageCount); + assertThat(mongoTemplate.count(new Query(), NewAction.class)).isEqualTo(actionCount); + } +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/resources/application-test.properties b/app/server/appsmith-server/src/test/resources/application-test.properties index 6aea70a3e6..0777b1f8d6 100644 --- a/app/server/appsmith-server/src/test/resources/application-test.properties +++ b/app/server/appsmith-server/src/test/resources/application-test.properties @@ -1,2 +1,2 @@ # embedded mongo DB version which is used during junit tests -de.flapdoodle.mongodb.embedded.version=5.0.14 \ No newline at end of file +de.flapdoodle.mongodb.embedded.version=5.0.5 \ No newline at end of file diff --git a/app/server/envs/dev.env.example b/app/server/envs/dev.env.example index 938cedbb92..0619bcb8fc 100644 --- a/app/server/envs/dev.env.example +++ b/app/server/envs/dev.env.example @@ -1,6 +1,6 @@ #!/bin/sh -APPSMITH_MONGODB_URI="mongodb://localhost:27017/appsmith" +APPSMITH_MONGODB_URI="mongodb://localhost:27017/appsmith?replicaSet=appsmith-replica-set" APPSMITH_REDIS_URL="redis://127.0.0.1:6379"