From: Sébastien Lesaint Date: Wed, 28 Mar 2018 14:41:17 +0000 (+0200) Subject: GOV-331 trigger views refresh on api/organizations/delete X-Git-Tag: 7.5~1389 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=80647004c959de65f48228e75e350df43f155448;p=sonarqube.git GOV-331 trigger views refresh on api/organizations/delete --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java index b599d89fada..3a486a15bd2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java @@ -21,6 +21,9 @@ package org.sonar.server.organization.ws; import java.util.Collection; import java.util.List; +import java.util.Set; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Scopes; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; @@ -35,6 +38,8 @@ import org.sonar.server.component.ComponentCleanerService; import org.sonar.server.organization.DefaultOrganization; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.OrganizationFlags; +import org.sonar.server.project.Project; +import org.sonar.server.project.ProjectLifeCycleListeners; import org.sonar.server.qualityprofile.QProfileFactory; import org.sonar.server.user.UserSession; import org.sonar.server.user.index.UserIndexer; @@ -56,9 +61,11 @@ public class DeleteAction implements OrganizationsWsAction { private final OrganizationFlags organizationFlags; private final UserIndexer userIndexer; private final QProfileFactory qProfileFactory; + private final ProjectLifeCycleListeners projectLifeCycleListeners; public DeleteAction(UserSession userSession, DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider, - ComponentCleanerService componentCleanerService, OrganizationFlags organizationFlags, UserIndexer userIndexer, QProfileFactory qProfileFactory) { + ComponentCleanerService componentCleanerService, OrganizationFlags organizationFlags, UserIndexer userIndexer, + QProfileFactory qProfileFactory, ProjectLifeCycleListeners projectLifeCycleListeners) { this.userSession = userSession; this.dbClient = dbClient; this.defaultOrganizationProvider = defaultOrganizationProvider; @@ -66,6 +73,7 @@ public class DeleteAction implements OrganizationsWsAction { this.organizationFlags = organizationFlags; this.userIndexer = userIndexer; this.qProfileFactory = qProfileFactory; + this.projectLifeCycleListeners = projectLifeCycleListeners; } @Override @@ -118,7 +126,21 @@ public class DeleteAction implements OrganizationsWsAction { roots.forEach(project -> dbClient.webhookDao().selectByProject(dbSession, project) .forEach(wh -> dbClient.webhookDeliveryDao().deleteByWebhook(dbSession, wh))); roots.forEach(project -> dbClient.webhookDao().deleteByProject(dbSession, project)); - componentCleanerService.delete(dbSession, roots); + try { + componentCleanerService.delete(dbSession, roots); + } finally { + Set projects = roots.stream() + .filter(DeleteAction::isMainProject) + .map(Project::from) + .collect(MoreCollectors.toSet()); + projectLifeCycleListeners.onProjectsDeleted(projects); + } + } + + private static boolean isMainProject(ComponentDto p) { + return Scopes.PROJECT.equals(p.scope()) + && Qualifiers.PROJECT.equals(p.qualifier()) + && p.getMainBranchProjectUuid() == null; } private void deletePermissions(DbSession dbSession, OrganizationDto organization) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java index e363905fc1b..c0c635ff151 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java @@ -19,10 +19,19 @@ */ package org.sonar.server.organization.ws; +import com.google.common.collect.ImmutableSet; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Arrays; import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.stream.IntStream; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; import org.sonar.api.config.internal.MapSettings; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; @@ -53,6 +62,8 @@ import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.organization.TestDefaultOrganizationProvider; import org.sonar.server.organization.TestOrganizationFlags; +import org.sonar.server.project.Project; +import org.sonar.server.project.ProjectLifeCycleListeners; import org.sonar.server.qualityprofile.QProfileFactory; import org.sonar.server.qualityprofile.QProfileFactoryImpl; import org.sonar.server.qualityprofile.index.ActiveRuleIndexer; @@ -65,16 +76,27 @@ import org.sonar.server.ws.WsActionTester; import static com.google.common.collect.ImmutableList.of; import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.sonar.api.resources.Qualifiers.APP; import static org.sonar.api.resources.Qualifiers.PROJECT; import static org.sonar.api.resources.Qualifiers.VIEW; +import static org.sonar.core.util.stream.MoreCollectors.toSet; import static org.sonar.db.permission.OrganizationPermission.ADMINISTER; import static org.sonar.db.user.UserTesting.newUserDto; import static org.sonar.db.webhook.WebhookDbTesting.newDto; import static org.sonar.server.organization.ws.OrganizationsWsSupport.PARAM_ORGANIZATION; +@RunWith(DataProviderRunner.class) public class DeleteActionTest { @Rule @@ -89,7 +111,7 @@ public class DeleteActionTest { private DbClient dbClient = db.getDbClient(); private DbSession dbSession = db.getSession(); private ResourceTypesRule resourceTypes = new ResourceTypesRule().setRootQualifiers(PROJECT, VIEW, APP).setAllQualifiers(PROJECT, VIEW, APP); - private ComponentCleanerService componentCleanerService = new ComponentCleanerService(db.getDbClient(), resourceTypes, mock(ProjectIndexers.class)); + private ComponentCleanerService spiedComponentCleanerService = spy(new ComponentCleanerService(db.getDbClient(), resourceTypes, mock(ProjectIndexers.class))); private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone().setEnabled(true); private TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db); private QProfileFactory qProfileFactory = new QProfileFactoryImpl(dbClient, mock(UuidFactory.class), System2.INSTANCE, mock(ActiveRuleIndexer.class)); @@ -98,8 +120,9 @@ public class DeleteActionTest { private final WebhookDbTester webhookDbTester = db.webhooks(); private final WebhookDeliveryDao deliveryDao = dbClient.webhookDeliveryDao(); private final WebhookDeliveryDbTester webhookDeliveryDbTester = db.webhookDelivery(); + private ProjectLifeCycleListeners projectLifeCycleListeners = mock(ProjectLifeCycleListeners.class); private WsActionTester wsTester = new WsActionTester( - new DeleteAction(userSession, dbClient, defaultOrganizationProvider, componentCleanerService, organizationFlags, userIndexer, qProfileFactory)); + new DeleteAction(userSession, dbClient, defaultOrganizationProvider, spiedComponentCleanerService, organizationFlags, userIndexer, qProfileFactory, projectLifeCycleListeners)); @Test public void test_definition() { @@ -177,6 +200,7 @@ public class DeleteActionTest { UserDto userReloaded = dbClient.userDao().selectUserById(dbSession, user.getId()); assertThat(userReloaded.getHomepageType()).isNull(); assertThat(userReloaded.getHomepageParameter()).isNull(); + verify(projectLifeCycleListeners).onProjectsDeleted(ImmutableSet.of(Project.from(project))); } @Test @@ -187,7 +211,11 @@ public class DeleteActionTest { expectedException.expect(IllegalStateException.class); expectedException.expectMessage("Organization support is disabled"); - wsTester.newRequest().execute(); + try { + wsTester.newRequest().execute(); + } finally { + verifyZeroInteractions(projectLifeCycleListeners); + } } @Test @@ -195,8 +223,12 @@ public class DeleteActionTest { expectedException.expect(UnauthorizedException.class); expectedException.expectMessage("Authentication is required"); - wsTester.newRequest() - .execute(); + try { + wsTester.newRequest() + .execute(); + } finally { + verifyNoMoreInteractions(projectLifeCycleListeners); + } } @Test @@ -206,7 +238,11 @@ public class DeleteActionTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("The 'organization' parameter is missing"); - wsTester.newRequest().execute(); + try { + wsTester.newRequest().execute(); + } finally { + verifyZeroInteractions(projectLifeCycleListeners); + } } @Test @@ -216,7 +252,11 @@ public class DeleteActionTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Default Organization can't be deleted"); - sendRequest(db.getDefaultOrganization()); + try { + sendRequest(db.getDefaultOrganization()); + } finally { + verifyZeroInteractions(projectLifeCycleListeners); + } } @Test @@ -226,7 +266,11 @@ public class DeleteActionTest { expectedException.expect(NotFoundException.class); expectedException.expectMessage("Organization with key 'foo' not found"); - sendRequest("foo"); + try { + sendRequest("foo"); + } finally { + verifyZeroInteractions(projectLifeCycleListeners); + } } @Test @@ -237,7 +281,11 @@ public class DeleteActionTest { expectedException.expect(ForbiddenException.class); expectedException.expectMessage("Insufficient privileges"); - sendRequest(organization); + try { + sendRequest(organization); + } finally { + verifyZeroInteractions(projectLifeCycleListeners); + } } @Test @@ -248,7 +296,11 @@ public class DeleteActionTest { expectedException.expect(ForbiddenException.class); expectedException.expectMessage("Insufficient privileges"); - sendRequest(organization); + try { + sendRequest(organization); + } finally { + verifyZeroInteractions(projectLifeCycleListeners); + } } @Test @@ -259,7 +311,11 @@ public class DeleteActionTest { expectedException.expect(ForbiddenException.class); expectedException.expectMessage("Insufficient privileges"); - sendRequest(organization); + try { + sendRequest(organization); + } finally { + verifyZeroInteractions(projectLifeCycleListeners); + } } @Test @@ -270,6 +326,7 @@ public class DeleteActionTest { sendRequest(organization); verifyOrganizationDoesNotExist(organization); + verify(projectLifeCycleListeners).onProjectsDeleted(emptySet()); } @Test @@ -280,6 +337,7 @@ public class DeleteActionTest { sendRequest(organization); verifyOrganizationDoesNotExist(organization); + verify(projectLifeCycleListeners).onProjectsDeleted(emptySet()); } @Test @@ -290,27 +348,43 @@ public class DeleteActionTest { sendRequest(organization); verifyOrganizationDoesNotExist(organization); + verify(projectLifeCycleListeners).onProjectsDeleted(emptySet()); } @Test - public void delete_components_of_specified_organization() { + @UseDataProvider("OneOrMoreIterations") + public void delete_components_of_specified_organization(int numberOfIterations) { OrganizationDto organization = db.organizations().insert(); - ComponentDto project = db.components().insertPrivateProject(organization); - ComponentDto module = db.components().insertComponent(ComponentTesting.newModuleDto(project)); - ComponentDto directory = db.components().insertComponent(ComponentTesting.newDirectory(module, "a/b")); - ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(module, directory)); - ComponentDto view = db.components().insertView(organization); - ComponentDto subview1 = db.components().insertComponent(ComponentTesting.newSubView(view, "v1", "ksv1")); - ComponentDto subview2 = db.components().insertComponent(ComponentTesting.newSubView(subview1, "v2", "ksv2")); - ComponentDto application = db.components().insertApplication(organization); - ComponentDto projectCopy = db.components().insertComponent(ComponentTesting.newProjectCopy("pc1", project, subview1)); - ComponentDto projectCopyForApplication = db.components().insertComponent(ComponentTesting.newProjectCopy("pc2", project, application)); + Set projects = IntStream.range(0, numberOfIterations).mapToObj(i -> { + ComponentDto project = db.components().insertPrivateProject(organization); + ComponentDto module = db.components().insertComponent(ComponentTesting.newModuleDto(project)); + ComponentDto directory = db.components().insertComponent(ComponentTesting.newDirectory(module, "a/b" + i)); + ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(module, directory)); + ComponentDto view = db.components().insertView(organization); + ComponentDto subview1 = db.components().insertComponent(ComponentTesting.newSubView(view, "v1" + i, "ksv1" + i)); + ComponentDto subview2 = db.components().insertComponent(ComponentTesting.newSubView(subview1, "v2" + i, "ksv2" + i)); + ComponentDto application = db.components().insertApplication(organization); + ComponentDto projectCopy = db.components().insertComponent(ComponentTesting.newProjectCopy("pc1" + i, project, subview1)); + ComponentDto projectCopyForApplication = db.components().insertComponent(ComponentTesting.newProjectCopy("pc2" + i, project, application)); + ComponentDto branch = db.components().insertProjectBranch(project); + return project; + }).collect(toSet()); + logInAsAdministrator(organization); sendRequest(organization); verifyOrganizationDoesNotExist(organization); assertThat(db.countRowsOfTable(db.getSession(), "projects")).isZero(); + verify(projectLifeCycleListeners).onProjectsDeleted(projects.stream().map(Project::from).collect(toSet())); + } + + @DataProvider + public static Object[][] OneOrMoreIterations() { + return new Object[][] { + {1}, + {1 + new Random().nextInt(10)}, + }; } @Test @@ -325,6 +399,7 @@ public class DeleteActionTest { verifyOrganizationDoesNotExist(organization); assertThat(db.countRowsOfTable(db.getSession(), "projects")).isZero(); assertThat(db.countRowsOfTable(db.getSession(), "project_branches")).isZero(); + verify(projectLifeCycleListeners).onProjectsDeleted(ImmutableSet.of(Project.from(project))); } @Test @@ -377,6 +452,7 @@ public class DeleteActionTest { .extracting(row -> (String) row.get("role")) .doesNotContain("u1", "u3", "u4", "u5") .contains("not deleted u1", "not deleted u3", "not deleted u4", "not deleted u5"); + verify(projectLifeCycleListeners).onProjectsDeleted(ImmutableSet.of(Project.from(projectDto))); } @Test @@ -399,6 +475,7 @@ public class DeleteActionTest { assertThat(db.getDbClient().organizationMemberDao().select(db.getSession(), otherOrg.getUuid(), user1.getId())).isPresent(); assertThat(userIndex.search(UserQuery.builder().setOrganizationUuid(org.getUuid()).build(), new SearchOptions()).getTotal()).isEqualTo(0); assertThat(userIndex.search(UserQuery.builder().setOrganizationUuid(otherOrg.getUuid()).build(), new SearchOptions()).getTotal()).isEqualTo(1); + verify(projectLifeCycleListeners).onProjectsDeleted(emptySet()); } @Test @@ -441,6 +518,39 @@ public class DeleteActionTest { // Check built-in quality gate is still available in other organization assertThat(db.getDbClient().qualityGateDao().selectByOrganizationAndName(db.getSession(), otherOrganization, "Sonar way")).isNotNull(); + verify(projectLifeCycleListeners).onProjectsDeleted(emptySet()); + } + + @Test + @UseDataProvider("indexOfFailingProjectDeletion") + public void projectLifeCycleListener_are_notified_even_if_deletion_of_a_project_throws_an_Exception(int failingProjectIndex) { + OrganizationDto organization = db.organizations().insert(); + ComponentDto[] projects = new ComponentDto[] { + db.components().insertPrivateProject(organization), + db.components().insertPrivateProject(organization), + db.components().insertPrivateProject(organization) + }; + logInAsAdministrator(organization); + RuntimeException expectedException = new RuntimeException("Faking deletion of 2nd project throwing an exception"); + doThrow(expectedException) + .when(spiedComponentCleanerService).delete(any(), eq(projects[failingProjectIndex])); + + try { + sendRequest(organization); + fail("A RuntimeException should have been thrown"); + } catch (RuntimeException e) { + assertThat(e).isSameAs(expectedException); + verify(projectLifeCycleListeners).onProjectsDeleted(Arrays.stream(projects).map(Project::from).collect(toSet())); + } + } + + @DataProvider + public static Object[][] indexOfFailingProjectDeletion() { + return new Object[][] { + {0}, + {1}, + {2} + }; } private void verifyOrganizationDoesNotExist(OrganizationDto organization) {