From 66bf23106dff1c9a93cc20ede76809170c092452 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 28 Jul 2020 17:54:06 +0530 Subject: [PATCH 1/2] Fix cloning fails in some cases for organizations (#159) * Fix cloning fails in some cases for organizations Cloning currently fails in cases like the following: - Application with no pages - Pages with no actions - Pages with more than one action * Remove debug naming of cloned datasources * Add test for organization cloning * Add more tests for organization cloning * Fix potential race condition in adding pages to an application * Move db update call to add page to application, into repository * Use `getIdCriteria` to query for document's _id --- .../appsmith/server/constants/FieldName.java | 1 + .../server/domains/ApplicationPage.java | 3 + .../CustomApplicationRepository.java | 4 + .../CustomApplicationRepositoryImpl.java | 16 + .../services/ApplicationPageService.java | 5 +- .../services/ApplicationPageServiceImpl.java | 96 ++- .../solutions/ExamplesOrganizationCloner.java | 85 ++- .../ExamplesOrganizationClonerTests.java | 556 ++++++++++++++++++ 8 files changed, 712 insertions(+), 54 deletions(-) create mode 100644 app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java index a23f3a4911..fe535ffcaa 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java @@ -24,6 +24,7 @@ public class FieldName { public static String PROVIDER_ID = "providerId"; public static String CATEGORY = "category"; public static String PAGE = "page"; + public static String PAGES = "pages"; public static String SIZE = "size"; public static String ROLE = "role"; public static String DEFAULT_WIDGET_NAME = "MainContainer"; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationPage.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationPage.java index ae0bc9f8b9..f6a6d65194 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationPage.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationPage.java @@ -1,5 +1,6 @@ package com.appsmith.server.domains; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -9,9 +10,11 @@ import lombok.ToString; @Setter @ToString @NoArgsConstructor +@AllArgsConstructor public class ApplicationPage { String id; Boolean isDefault; + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepository.java index 5ac9c66a8b..8469b02ed5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepository.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepository.java @@ -2,6 +2,8 @@ package com.appsmith.server.repositories; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.Page; +import com.mongodb.client.result.UpdateResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -16,4 +18,6 @@ public interface CustomApplicationRepository extends AppsmithRepository findByOrganizationId(String orgId, AclPermission permission); Flux findByMultipleOrganizationIds(Set orgIds, AclPermission permission); + + Mono addPageToApplication(Application application, Page page, boolean isDefault); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepositoryImpl.java index 8222ea2ca0..c9b41f8bad 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepositoryImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepositoryImpl.java @@ -2,14 +2,20 @@ package com.appsmith.server.repositories; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.PolicyGenerator; +import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationPage; +import com.appsmith.server.domains.Page; import com.appsmith.server.domains.QApplication; +import com.mongodb.client.result.UpdateResult; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -64,4 +70,14 @@ public class CustomApplicationRepositoryImpl extends BaseAppsmithRepositoryImpl< return queryAll(List.of(orgIdsCriteria), permission); } + @Override + public Mono addPageToApplication(Application application, Page page, boolean isDefault) { + final ApplicationPage applicationPage = new ApplicationPage(page.getId(), isDefault); + return mongoOperations.updateFirst( + Query.query(getIdCriteria(application.getId())), + new Update().addToSet(FieldName.PAGES, applicationPage), + Application.class + ); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java index 6f4185bfcb..537a575a26 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java @@ -2,12 +2,13 @@ package com.appsmith.server.services; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Page; +import com.mongodb.client.result.UpdateResult; import reactor.core.publisher.Mono; public interface ApplicationPageService { Mono createPage(Page page); - Mono addPageToApplication(Mono applicationMono, Page page, Boolean isDefault); + Mono addPageToApplication(Application application, Page page, Boolean isDefault); Mono getPage(String pageId, Boolean viewMode); @@ -19,5 +20,7 @@ public interface ApplicationPageService { Mono makePageDefault(String applicationId, String pageId); + Mono cloneApplication(Application application); + Mono deleteApplication(String id); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java index b223d7c8a1..348db488e3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java @@ -13,11 +13,15 @@ import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.repositories.ApplicationRepository; +import com.mongodb.client.result.UpdateResult; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -40,18 +44,22 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { private final AnalyticsService analyticsService; private final PolicyGenerator policyGenerator; + private final ApplicationRepository applicationRepository; + public ApplicationPageServiceImpl(ApplicationService applicationService, PageService pageService, SessionUserService sessionUserService, OrganizationService organizationService, AnalyticsService analyticsService, - PolicyGenerator policyGenerator) { + PolicyGenerator policyGenerator, + ApplicationRepository applicationRepository) { this.applicationService = applicationService; this.pageService = pageService; this.sessionUserService = sessionUserService; this.organizationService = organizationService; this.analyticsService = analyticsService; this.policyGenerator = policyGenerator; + this.applicationRepository = applicationRepository; } public Mono createPage(Page page) { @@ -87,34 +95,31 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { return pageMono .flatMap(pageService::createDefault) //After the page has been saved, update the application (save the page id inside the application) - .flatMap(savedPage -> - addPageToApplication(applicationMono, savedPage, false) - .thenReturn(savedPage)); + .zipWith(applicationMono) + .flatMap(tuple -> { + final Page savedPage = tuple.getT1(); + final Application application = tuple.getT2(); + return addPageToApplication(application, savedPage, false) + .thenReturn(savedPage); + }); } /** - * This function is called during page create in Page Service. It adds the newly created - * page to its ApplicationPages list. + * This function is called during page create in Page Service. It adds the given page to its ApplicationPages list. + * Note: It is assumed here that `application` is already checked for the MANAGE_APPLICATIONS policy. * - * @param applicationMono - * @param page - * @return Updated application + * @param application Application to which the page will be added. Should have an `id` already. + * @param page Page to be added to the application. Should have an `id` already. + * @return UpdateResult object with details on how many documents have been updated, which should be 0 or 1. */ - public Mono addPageToApplication(Mono applicationMono, Page page, Boolean isDefault) { - return applicationMono - .map(application -> { - List applicationPages = application.getPages(); - if (applicationPages == null) { - applicationPages = new ArrayList<>(); + @Override + public Mono addPageToApplication(Application application, Page page, Boolean isDefault) { + return applicationRepository.addPageToApplication(application, page, isDefault) + .doOnSuccess(result -> { + if (result.getModifiedCount() != 1) { + log.error("Add page to application didn't update anything, probably because application wasn't found."); } - ApplicationPage applicationPage = new ApplicationPage(); - applicationPage.setId(page.getId()); - applicationPage.setIsDefault(isDefault); - applicationPages.add(applicationPage); - application.setPages(applicationPages); - return application; - }) - .flatMap(applicationService::save); + }); } public Mono getPage(String pageId, Boolean viewMode) { @@ -246,10 +251,53 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { return pageService .createDefault(page) - .flatMap(savedPage -> addPageToApplication(Mono.just(savedApplication), savedPage, true)); + .flatMap(savedPage -> addPageToApplication(savedApplication, savedPage, true)) + .then(applicationService.findById(savedApplication.getId(), READ_APPLICATIONS)); }); } + @Override + public Mono cloneApplication(Application application) { + if (!StringUtils.hasText(application.getName())) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.NAME)); + } + + String orgId = application.getOrganizationId(); + if (!StringUtils.hasText(orgId)) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORGANIZATION_ID)); + } + + // Clean the object so that it will be saved as a new application for the currently signed in user. + application.setId(null); + application.setPolicies(new HashSet<>()); + application.setPages(new ArrayList<>()); + + Mono userMono = sessionUserService.getCurrentUser().cache(); + Mono applicationWithPoliciesMono = userMono + .flatMap(user -> { + Mono orgMono = organizationService.findById(orgId, ORGANIZATION_MANAGE_APPLICATIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, orgId))); + + return orgMono.map(org -> { + application.setOrganizationId(org.getId()); + // At the organization level, filter out all the application specific policies and apply them + // to the new application that we are creating. + Set policySet = org.getPolicies().stream() + .filter(policy -> + policy.getPermission().equals(ORGANIZATION_MANAGE_APPLICATIONS.getValue()) || + policy.getPermission().equals(ORGANIZATION_READ_APPLICATIONS.getValue()) + ).collect(Collectors.toSet()); + + Set documentPolicies = policyGenerator.getAllChildPolicies(policySet, Organization.class, Application.class); + application.setPolicies(documentPolicies); + return application; + }); + }); + + return applicationWithPoliciesMono + .flatMap(applicationService::createDefault); + } + private void generateAndSetPagePolicies(Application application, User user, Page page) { Set policySet = application.getPolicies().stream() .filter(policy -> policy.getPermission().equals(MANAGE_APPLICATIONS.getValue()) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java index c3aa9d3e46..ee62513f7b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java @@ -3,8 +3,10 @@ package com.appsmith.server.solutions; import com.appsmith.external.models.BaseDomain; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Action; +import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Organization; +import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; @@ -94,7 +96,8 @@ public class ExamplesOrganizationCloner { * @param user The user who will own the new cloned organization. * @return Publishes the newly created organization. */ - private Mono cloneOrganizationForUser(String templateOrganizationId, User user) { + public Mono cloneOrganizationForUser(String templateOrganizationId, User user) { + log.info("Cloning organization id {}", templateOrganizationId); return organizationRepository .findById(templateOrganizationId) .doOnSuccess(organization -> { @@ -144,46 +147,70 @@ public class ExamplesOrganizationCloner { .findByOrganizationIdAndIsPublicTrue(fromOrganizationId) .flatMap(application -> { final String templateApplicationId = application.getId(); - makePristine(application); application.setOrganizationId(toOrganizationId); - if (!CollectionUtils.isEmpty(application.getPages())) { - application.getPages().clear(); - } - return Flux.combineLatest( - pageRepository.findByApplicationId(templateApplicationId), - applicationPageService.createApplication(application).cache(), - (page, savedApplication) -> { - log.info("Cloned application {} into new application {}", templateApplicationId, savedApplication.getId()); - page.setApplicationId(savedApplication.getId()); - return page; - } - ); + return doCloneApplication(application, templateApplicationId); }) .flatMap(page -> { final String templatePageId = page.getId(); makePristine(page); - return Flux.combineLatest( - actionRepository.findByPageId(templatePageId), - applicationPageService.createPage(page).cache(), - (action, savedPage) -> { - action.setPageId(savedPage.getId()); - return action; - } - ); + return applicationPageService + .createPage(page) + .flatMap(page1 -> { + log.info("Cloned into new page {}", page1); + return applicationRepository.findById(page.getApplicationId()) + .map(application -> { + log.info("Application after page got cloned: {}", application); + return page1; + }); + }) + .flatMapMany( + savedPage -> actionRepository + .findByPageId(templatePageId) + .map(action -> { + log.info("Preparing action for cloning {} {}.", action.getName(), action.getId()); + action.setPageId(savedPage.getId()); + return action; + }) + ); }) - .zipWith(cloneDatasourcesMono) - .flatMap(tuple -> { - final Action action = tuple.getT1(); - final Map newDatasourcesByTemplateId = tuple.getT2(); + .flatMap(action -> { + log.info("Creating clone of action {}", action.getId()); makePristine(action); action.setOrganizationId(toOrganizationId); action.setCollectionId(null); - action.setDatasource(newDatasourcesByTemplateId.get(action.getDatasource().getId())); - return actionService.create(action); + Mono actionMono = Mono.just(action); + final Datasource datasourceInsideAction = action.getDatasource(); + if (datasourceInsideAction != null) { + if (datasourceInsideAction.getId() != null) { + actionMono = cloneDatasourcesMono + .map(newDatasourcesByTemplateId -> { + action.setDatasource(newDatasourcesByTemplateId.get(datasourceInsideAction.getId())); + return action; + }); + } else { + datasourceInsideAction.setOrganizationId(toOrganizationId); + } + } + return actionMono.flatMap(actionService::create); }) + .then(cloneDatasourcesMono) // Run the datasource cloning mono if it isn't already done. .then(); } + private Flux doCloneApplication(Application application, String templateApplicationId) { + return applicationPageService + .cloneApplication(application) + .flatMapMany( + savedApplication -> pageRepository + .findByApplicationId(templateApplicationId) + .map(page -> { + log.info("Preparing page for cloning {} {}.", page.getName(), page.getId()); + page.setApplicationId(savedApplication.getId()); + return page; + }) + ); + } + /** * Clone all the datasources (except deleted ones) from one organization to another. Publishes a map where the keys * are IDs of datasources that were copied (source IDs), and the values are the cloned datasource objects which @@ -202,7 +229,7 @@ public class ExamplesOrganizationCloner { } makePristine(datasource); datasource.setOrganizationId(toOrganizationId); - datasource.setName(datasource.getName() + " cloned " + Math.random()); + datasource.setName(datasource.getName()); return Mono.zip( Mono.just(templateDatasourceId), datasourceService.create(datasource) diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java new file mode 100644 index 0000000000..89f9613ccb --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java @@ -0,0 +1,556 @@ +package com.appsmith.server.solutions; + +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.Property; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Action; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.Organization; +import com.appsmith.server.domains.Page; +import com.appsmith.server.domains.Plugin; +import com.appsmith.server.helpers.MockPluginExecutor; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.repositories.PluginRepository; +import com.appsmith.server.services.ActionCollectionService; +import com.appsmith.server.services.ActionService; +import com.appsmith.server.services.ApplicationPageService; +import com.appsmith.server.services.ApplicationService; +import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.OrganizationService; +import com.appsmith.server.services.PageService; +import com.appsmith.server.services.SessionUserService; +import com.appsmith.server.services.UserService; +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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.security.test.context.support.WithUserDetails; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.LinkedMultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS; +import static com.appsmith.server.acl.AclPermission.READ_DATASOURCES; +import static com.appsmith.server.acl.AclPermission.READ_PAGES; +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@RunWith(SpringRunner.class) +@SpringBootTest +@DirtiesContext +public class ExamplesOrganizationClonerTests { + + @Autowired + UserService userService; + + @Autowired + private ExamplesOrganizationCloner examplesOrganizationCloner; + + @Autowired + private ApplicationService applicationService; + + @Autowired + private DatasourceService datasourceService; + + @Autowired + private OrganizationService organizationService; + + @Autowired + private ApplicationPageService applicationPageService; + + @Autowired + private SessionUserService sessionUserService; + + @Autowired + private ActionService actionService; + + @Autowired + private PageService pageService; + + @Autowired + private ActionCollectionService actionCollectionService; + + @Autowired + private PluginRepository pluginRepository; + + @MockBean + private PluginExecutorHelper pluginExecutorHelper; + + private Plugin installedPlugin; + + private static class OrganizationData { + Organization organization; + List applications = new ArrayList<>(); + List datasources = new ArrayList<>(); + List actions = new ArrayList<>(); + } + + public Mono loadOrganizationData(Organization organization) { + final OrganizationData data = new OrganizationData(); + data.organization = organization; + + return Mono + .when( + applicationService + .findByOrganizationId(organization.getId(), READ_APPLICATIONS) + .map(data.applications::add), + datasourceService + .findAllByOrganizationId(organization.getId(), READ_DATASOURCES) + .map(data.datasources::add), + getActionsInOrganization(organization) + .map(data.actions::add) + ) + .thenReturn(data); + } + + @Before + public void setup() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + installedPlugin = pluginRepository.findByPackageName("installed-plugin").block(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneEmptyOrganization() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization"); + final Mono resultMono = organizationService.create(newOrganization) + .zipWith(sessionUserService.getCurrentUser()) + .flatMap(tuple -> + examplesOrganizationCloner.cloneOrganizationForUser(tuple.getT1().getId(), tuple.getT2())) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).isEmpty(); + assertThat(data.datasources).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithItsContents() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + Application app1 = new Application(); + app1.setName("1 - public app"); + app1.setOrganizationId(organization.getId()); + app1.setIsPublic(true); + + Application app2 = new Application(); + app2.setOrganizationId(organization.getId()); + app2.setName("2 - private app"); + + return Mono.when( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).hasSize(1); + assertThat(map(data.applications, Application::getName)).containsExactly("1 - public app"); + assertThat(data.applications.get(0).getPages()).hasSize(1); + + assertThat(data.datasources).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithOnlyPublicApplications() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + Application app1 = new Application(); + app1.setName("1 - public app more"); + app1.setOrganizationId(organization.getId()); + app1.setIsPublic(true); + + Application app2 = new Application(); + app2.setOrganizationId(organization.getId()); + app2.setName("2 - another public app more"); + app2.setIsPublic(true); + + return Mono.zip( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2).flatMap(application -> { + final Page newPage = new Page(); + newPage.setName("The New Page"); + newPage.setApplicationId(application.getId()); + return applicationPageService.createPage(newPage); + }) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).hasSize(2); + assertThat(map(data.applications, Application::getName)).containsExactlyInAnyOrder( + "1 - public app more", + "2 - another public app more" + ); + + for (final Application app : data.applications) { + if ("2 - another public app more".equals(app.getName())) { + assertThat(app.getPages()).hasSize(2); + } else { + assertThat(app.getPages()).hasSize(1); + } + } + + assertThat(data.datasources).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithOnlyPrivateApplications() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + Application app1 = new Application(); + app1.setName("1 - private app more"); + app1.setOrganizationId(organization.getId()); + + Application app2 = new Application(); + app2.setOrganizationId(organization.getId()); + app2.setName("2 - another private app more"); + + return Mono.when( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).isEmpty(); + assertThat(data.datasources).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithOnlyDatasources() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + final Datasource ds1 = new Datasource(); + ds1.setName("datasource 1"); + ds1.setOrganizationId(organization.getId()); + final DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + ds1.setDatasourceConfiguration(datasourceConfiguration); + datasourceConfiguration.setUrl("http://httpbin.org/get"); + datasourceConfiguration.setHeaders(List.of( + new Property("X-Answer", "42") + )); + + final Datasource ds2 = new Datasource(); + ds2.setName("datasource 2"); + ds2.setOrganizationId(organization.getId()); + + return Mono.when( + datasourceService.create(ds1), + datasourceService.create(ds2) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.datasources).hasSize(2); + assertThat(map(data.datasources, Datasource::getName)).containsExactlyInAnyOrder( + "datasource 1", + "datasource 2" + ); + + final Datasource ds1 = data.datasources.stream() + .filter(datasource -> "datasource 1".equals(datasource.getName())) + .findFirst() + .orElseThrow(); + assertThat(ds1.getDatasourceConfiguration().getUrl()).isEqualTo("http://httpbin.org/get"); + assertThat(ds1.getDatasourceConfiguration().getHeaders()).containsOnly( + new Property("X-Answer", "42") + ); + + assertThat(data.applications).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithDatasourcesAndApplications() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + final Application app1 = new Application(); + app1.setName("first application"); + app1.setOrganizationId(organization.getId()); + app1.setIsPublic(true); + + final Application app2 = new Application(); + app2.setName("second application"); + app2.setOrganizationId(organization.getId()); + app2.setIsPublic(true); + + final Datasource ds1 = new Datasource(); + ds1.setName("datasource 1"); + ds1.setOrganizationId(organization.getId()); + + final Datasource ds2 = new Datasource(); + ds2.setName("datasource 2"); + ds2.setOrganizationId(organization.getId()); + + return Mono.when( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2), + datasourceService.create(ds1), + datasourceService.create(ds2) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).hasSize(2); + assertThat(map(data.applications, Application::getName)).containsExactlyInAnyOrder( + "first application", + "second application" + ); + + assertThat(data.datasources).hasSize(2); + assertThat(map(data.datasources, Datasource::getName)).containsExactlyInAnyOrder( + "datasource 1", + "datasource 2" + ); + + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithDatasourcesAndApplicationsAndActions() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + final Application app1 = new Application(); + app1.setName("first application"); + app1.setOrganizationId(organization.getId()); + app1.setIsPublic(true); + + final Application app2 = new Application(); + app2.setName("second application"); + app2.setOrganizationId(organization.getId()); + app2.setIsPublic(true); + + final Datasource ds1 = new Datasource(); + ds1.setName("datasource 1"); + ds1.setOrganizationId(organization.getId()); + ds1.setPluginId(installedPlugin.getId()); + + final Datasource ds2 = new Datasource(); + ds2.setName("datasource 2"); + ds2.setOrganizationId(organization.getId()); + ds2.setPluginId(installedPlugin.getId()); + + return Mono + .zip( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2), + datasourceService.create(ds1), + datasourceService.create(ds2) + ) + .flatMap(tuple1 -> { + final Application app = tuple1.getT1(); + final String pageId1 = app.getPages().get(0).getId(); + final Datasource ds1Again = tuple1.getT3(); + + final Action action1 = new Action(); + action1.setName("action1"); + action1.setPageId(pageId1); + action1.setOrganizationId(organization.getId()); + action1.setDatasource(ds1Again); + action1.setPluginId(installedPlugin.getId()); + + final Action action2 = new Action(); + action2.setPageId(pageId1); + action2.setName("action2"); + action2.setOrganizationId(organization.getId()); + action2.setDatasource(ds1Again); + action2.setPluginId(installedPlugin.getId()); + + final Application app2Again = tuple1.getT2(); + final String pageId2 = app2Again.getPages().get(0).getId(); + final Datasource ds2Again = tuple1.getT4(); + + final Action action3 = new Action(); + action3.setName("action3"); + action3.setPageId(pageId2); + action3.setOrganizationId(organization.getId()); + action3.setDatasource(ds2Again); + action3.setPluginId(installedPlugin.getId()); + + final Action action4 = new Action(); + action4.setPageId(pageId2); + action4.setName("action4"); + action4.setOrganizationId(organization.getId()); + action4.setDatasource(ds2Again); + action4.setPluginId(installedPlugin.getId()); + + return Mono.when( + actionCollectionService.createAction(action1), + actionCollectionService.createAction(action2), + actionCollectionService.createAction(action3), + actionCollectionService.createAction(action4) + ); + }) + .then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).hasSize(2); + assertThat(map(data.applications, Application::getName)).containsExactlyInAnyOrder( + "first application", + "second application" + ); + + assertThat(data.datasources).hasSize(2); + assertThat(map(data.datasources, Datasource::getName)).containsExactlyInAnyOrder( + "datasource 1", + "datasource 2" + ); + + assertThat(data.actions).hasSize(4); + assertThat(map(data.actions, Action::getName)).containsExactlyInAnyOrder( + "action1", + "action2", + "action3", + "action4" + ); + }) + .verifyComplete(); + } + + private List map(List list, Function fn) { + return list.stream().map(fn).collect(Collectors.toList()); + } + + private Flux getActionsInOrganization(Organization organization) { + return applicationService + .findByOrganizationId(organization.getId(), READ_APPLICATIONS) + .flatMap(application -> pageService.findByApplicationId(application.getId(), READ_PAGES)) + .flatMap(page -> actionService.get(new LinkedMultiValueMap( + Map.of(FieldName.PAGE_ID, Collections.singletonList(page.getId()))))); + } +} From 7eda0f02016a5599b6d1712b9f0ad617d71299b8 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Tue, 28 Jul 2020 18:21:40 +0530 Subject: [PATCH 2/2] Making the pre-release tag configurable based on the tag (#184) --- .github/workflows/github-release.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 66d53b2030..e36d834dae 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -121,6 +121,21 @@ jobs: steps: # Creating the release on Github + - name: Get the version + id: get_version + run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + + # If the tag has the string "beta", then mark the Github release as a pre-release + - name: Get the version + id: get_release + run: | + STATUS=false + if [[ ! ${{steps.get_version.outputs.tag}} == *"beta"* ]]; then + STATUS=true + fi + + echo ::set-output name=status::${STATUS} + - name: Create Release id: create_release uses: actions/create-release@v1 @@ -130,5 +145,5 @@ jobs: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: false - prerelease: false + prerelease: ${{steps.get_release.outputs.status}} \ No newline at end of file