aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2018-03-28 16:41:17 +0200
committerSonarTech <sonartech@sonarsource.com>2018-04-06 20:21:52 +0200
commit80647004c959de65f48228e75e350df43f155448 (patch)
tree04a4415a6fac437b28028fd5fcfefea75ffa4d38
parent182432a56dd115b1f7b76aba1eb91ee7387172db (diff)
downloadsonarqube-80647004c959de65f48228e75e350df43f155448.tar.gz
sonarqube-80647004c959de65f48228e75e350df43f155448.zip
GOV-331 trigger views refresh on api/organizations/delete
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java26
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java154
2 files changed, 156 insertions, 24 deletions
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<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) {
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<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) {