]> source.dussan.org Git - sonarqube.git/commitdiff
GOV-331 trigger views refresh on api/organizations/delete
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 28 Mar 2018 14:41:17 +0000 (16:41 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 6 Apr 2018 18:21:52 +0000 (20:21 +0200)
server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java
server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java

index b599d89fada776f5ca1cf002d932f09f83fd42db..3a486a15bd2b512ed7350ea766ae7c65055579c7 100644 (file)
@@ -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<Project> 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) {
index e363905fc1b13449a595a38782a2107e87aa21e1..c0c635ff1510129a97542828f0886446d7fe9199 100644 (file)
  */
 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<ComponentDto> 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) {