Browse Source

GOV-331 trigger views refresh on api/projects/bulk_delete

tags/7.5
Sébastien Lesaint 6 years ago
parent
commit
182432a56d

+ 5
- 2
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java View File

@@ -47,6 +47,7 @@ import static org.sonar.core.util.stream.MoreCollectors.toList;
import static org.sonar.db.DaoDatabaseUtils.buildLikeValue;
import static org.sonar.db.DatabaseUtils.checkThatNotTooManyConditions;
import static org.sonar.db.DatabaseUtils.executeLargeInputs;
import static org.sonar.db.DatabaseUtils.executeLargeInputsIntoSet;
import static org.sonar.db.DatabaseUtils.executeLargeUpdates;
import static org.sonar.db.WildcardPosition.BEFORE_AND_AFTER;
import static org.sonar.db.component.ComponentDto.generateBranchKey;
@@ -268,8 +269,10 @@ public class ComponentDao implements Dao {
/**
* Used by Governance
*/
public Set<String> selectViewKeysWithEnabledCopyOfProject(DbSession session, String projectUuid) {
return mapper(session).selectViewKeysWithEnabledCopyOfProject(projectUuid);
public Set<String> selectViewKeysWithEnabledCopyOfProject(DbSession session, Set<String> projectUuids) {
return executeLargeInputsIntoSet(projectUuids,
partition -> mapper(session).selectViewKeysWithEnabledCopyOfProject(partition),
i -> i);
}

public List<String> selectProjectsFromView(DbSession session, String viewUuid, String projectViewUuid) {

+ 1
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java View File

@@ -121,7 +121,7 @@ public interface ComponentMapper {
*/
List<KeyWithUuidDto> selectUuidsByKeyFromProjectKey(@Param("projectKey") String projectKey);

Set<String> selectViewKeysWithEnabledCopyOfProject(@Param("projectUuid") String projectUuid);
Set<String> selectViewKeysWithEnabledCopyOfProject(@Param("projectUuids") Collection<String> projectUuids);

/**
* Return technical projects from a view or a sub-view

+ 2
- 1
server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml View File

@@ -409,7 +409,8 @@
leaf.qualifier = 'TRK'
and leaf.scope = 'FIL'
and leaf.enabled = ${_true}
and leaf.copy_component_uuid = #{projectUuid,jdbcType=VARCHAR}
and leaf.copy_component_uuid in
<foreach collection="projectUuids" open="(" close=")" item="uuid" separator=",">#{uuid,jdbcType=VARCHAR}</foreach>
where
p.enabled = ${_true}
and p.uuid = leaf.project_uuid

+ 141
- 17
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java View File

@@ -35,6 +35,7 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.assertj.core.api.ListAssert;
import org.junit.Rule;
@@ -50,12 +51,15 @@ import org.sonar.db.RowNotFoundException;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.source.FileSourceDto;

import static com.google.common.collect.ImmutableSet.of;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
@@ -658,10 +662,27 @@ public class ComponentDaoTest {
}

@Test
public void selectViewKeysWithEnabledCopyOfProject_returns_empty_when_there_is_no_view() {
String projectUuid = randomAlphabetic(5);
public void selectViewKeysWithEnabledCopyOfProject_returns_empty_when_set_is_empty() {
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, emptySet()))
.isEmpty();
}

@Test
@UseDataProvider("oneOrMoreProjects")
public void selectViewKeysWithEnabledCopyOfProject_returns_empty_when_there_is_no_view(int projectCount) {
Set<String> projectUuids = IntStream.range(0, projectCount)
.mapToObj(i -> randomAlphabetic(5))
.collect(toSet());

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, projectUuids)).isEmpty();
}

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, projectUuid)).isEmpty();
@DataProvider
public static Object[][] oneOrMoreProjects() {
return new Object[][] {
{1},
{1 + new Random().nextInt(10)}
};
}

@Test
@@ -672,9 +693,47 @@ public class ComponentDaoTest {
ComponentDto view = insertView(organization, rootViewQualifier);
insertProjectCopy(view, project);

Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid()));

assertThat(keys).containsOnly(view.getDbKey());

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid())))
.isEqualTo(keys);
}

@Test
@UseDataProvider("portfolioOrApplicationRootViewQualifier")
public void selectViewKeysWithEnabledCopyOfProject_returns_root_views_with_direct_copy_of_projects(String rootViewQualifier) {
OrganizationDto organization = db.organizations().insert();
ComponentDto project1 = insertProject(organization);
ComponentDto project2 = insertProject(organization);
ComponentDto view = insertView(organization, rootViewQualifier);
insertProjectCopy(view, project1);
insertProjectCopy(view, project2);
ComponentDto view2 = insertView(organization, rootViewQualifier);
ComponentDto project3 = insertProject(organization);
insertProjectCopy(view2, project3);

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project1.uuid())))
.containsOnly(view.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project1.uuid())))
.containsOnly(view.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project2.uuid())))
.containsOnly(view.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid())))
.containsOnly(view.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project3.uuid())))
.containsOnly(view2.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project3.uuid())))
.containsOnly(view2.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, of(project2.uuid(), project1.uuid())))
.containsOnly(view.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid(), project1.uuid())))
.containsOnly(view.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, of(project1.uuid(), project3.uuid())))
.containsOnly(view.getDbKey(), view2.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project1.uuid(), project3.uuid())))
.containsOnly(view.getDbKey(), view2.getDbKey());
}

@Test
@@ -688,9 +747,12 @@ public class ComponentDaoTest {
ComponentDto view2 = insertView(organization, rootViewQualifier);
insertProjectCopy(view2, project2);

Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project2.uuid());
Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project2.uuid()));

assertThat(keys).containsOnly(view2.getDbKey());

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid())))
.isEqualTo(keys);
}

@Test
@@ -703,9 +765,12 @@ public class ComponentDaoTest {
ComponentDto view2 = insertView(organization, rootViewQualifier);
insertProjectCopy(view2, project, t -> t.setEnabled(false));

Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid()));

assertThat(keys).containsOnly(view1.getDbKey());

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid())))
.isEqualTo(keys);
}

@Test
@@ -718,9 +783,12 @@ public class ComponentDaoTest {
ComponentDto view2 = insertView(organization, rootViewQualifier);
insertProjectCopy(view2, project);

Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid()));

assertThat(keys).containsOnly(view2.getDbKey());

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid())))
.isEqualTo(keys);
}

@Test
@@ -732,9 +800,49 @@ public class ComponentDaoTest {
ComponentDto lowestSubview = insertSubviews(view);
insertProjectCopy(lowestSubview, project);

Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid()));

assertThat(keys).containsOnly(view.getDbKey());

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid())))
.isEqualTo(keys);
}

@Test
@UseDataProvider("portfolioOrApplicationRootViewQualifier")
public void selectViewKeysWithEnabledCopyOfProject_returns_root_views_with_indirect_copy_of_projects(String rootViewQualifier) {
OrganizationDto organization = db.organizations().insert();
ComponentDto project1 = insertProject(organization);
ComponentDto project2 = insertProject(organization);
ComponentDto view1 = insertView(organization, rootViewQualifier);
ComponentDto lowestSubview1 = insertSubviews(view1);
insertProjectCopy(lowestSubview1, project1);
insertProjectCopy(lowestSubview1, project2);
ComponentDto view2 = insertView(organization, rootViewQualifier);
ComponentDto lowestSubview2 = insertSubviews(view2);
ComponentDto project3 = insertProject(organization);
insertProjectCopy(lowestSubview2, project3);

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project1.uuid())))
.containsOnly(view1.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project1.uuid())))
.containsOnly(view1.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project2.uuid())))
.containsOnly(view1.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid())))
.containsOnly(view1.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project3.uuid())))
.containsOnly(view2.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project3.uuid())))
.containsOnly(view2.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, of(project2.uuid(), project1.uuid())))
.containsOnly(view1.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid(), project1.uuid())))
.containsOnly(view1.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, of(project1.uuid(), project3.uuid())))
.containsOnly(view1.getDbKey(), view2.getDbKey());
assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project1.uuid(), project3.uuid())))
.containsOnly(view1.getDbKey(), view2.getDbKey());
}

@Test
@@ -750,9 +858,12 @@ public class ComponentDaoTest {
ComponentDto lowestSubview2 = insertSubviews(view2);
insertProjectCopy(lowestSubview2, project2);

Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project2.uuid());
Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project2.uuid()));

assertThat(keys).containsOnly(view2.getDbKey());

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project2.uuid())))
.isEqualTo(keys);
}

@Test
@@ -767,9 +878,12 @@ public class ComponentDaoTest {
ComponentDto lowestSubview2 = insertSubviews(view2);
insertProjectCopy(lowestSubview2, project, t -> t.setEnabled(false));

Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid()));

assertThat(keys).containsOnly(view1.getDbKey());

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid())))
.isEqualTo(keys);
}

@Test
@@ -784,9 +898,12 @@ public class ComponentDaoTest {
ComponentDto lowestSubview2 = insertSubviews(view2);
insertProjectCopy(lowestSubview2, project);

Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, project.uuid());
Set<String> keys = underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, singleton(project.uuid()));

assertThat(keys).containsOnly(view2.getDbKey());

assertThat(underTest.selectViewKeysWithEnabledCopyOfProject(dbSession, shuffleWithInexistingUuids(project.uuid())))
.isEqualTo(keys);
}

@DataProvider
@@ -992,7 +1109,7 @@ public class ComponentDaoTest {

@Test
public void countByQuery_throws_IAE_if_too_many_component_ids() {
Set<Long> ids = LongStream.range(0L, 1_010L).boxed().collect(Collectors.toSet());
Set<Long> ids = LongStream.range(0L, 1_010L).boxed().collect(toSet());
ComponentQuery.Builder query = ComponentQuery.builder()
.setQualifiers(Qualifiers.PROJECT)
.setComponentIds(ids);
@@ -1002,7 +1119,7 @@ public class ComponentDaoTest {

@Test
public void countByQuery_throws_IAE_if_too_many_component_keys() {
Set<String> keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet());
Set<String> keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(toSet());
ComponentQuery.Builder query = ComponentQuery.builder()
.setQualifiers(Qualifiers.PROJECT)
.setComponentKeys(keys);
@@ -1012,7 +1129,7 @@ public class ComponentDaoTest {

@Test
public void countByQuery_throws_IAE_if_too_many_component_uuids() {
Set<String> uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet());
Set<String> uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(toSet());
ComponentQuery.Builder query = ComponentQuery.builder()
.setQualifiers(Qualifiers.PROJECT)
.setComponentUuids(uuids);
@@ -1258,7 +1375,7 @@ public class ComponentDaoTest {

@Test
public void selectByQuery_throws_IAE_if_too_many_component_ids() {
Set<Long> ids = LongStream.range(0L, 1_010L).boxed().collect(Collectors.toSet());
Set<Long> ids = LongStream.range(0L, 1_010L).boxed().collect(toSet());
ComponentQuery.Builder query = ComponentQuery.builder()
.setQualifiers(Qualifiers.PROJECT)
.setComponentIds(ids);
@@ -1268,7 +1385,7 @@ public class ComponentDaoTest {

@Test
public void selectByQuery_throws_IAE_if_too_many_component_keys() {
Set<String> keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet());
Set<String> keys = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(toSet());
ComponentQuery.Builder query = ComponentQuery.builder()
.setQualifiers(Qualifiers.PROJECT)
.setComponentKeys(keys);
@@ -1278,7 +1395,7 @@ public class ComponentDaoTest {

@Test
public void selectByQuery_throws_IAE_if_too_many_component_uuids() {
Set<String> uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(Collectors.toSet());
Set<String> uuids = IntStream.range(0, 1_010).mapToObj(String::valueOf).collect(toSet());
ComponentQuery.Builder query = ComponentQuery.builder()
.setQualifiers(Qualifiers.PROJECT)
.setComponentUuids(uuids);
@@ -1769,4 +1886,11 @@ public class ComponentDaoTest {
return underTest.selectByUuid(db.getSession(), uuid).get().isPrivate();
}

private static Set<String> shuffleWithInexistingUuids(String... uuids) {
return Stream.concat(
IntStream.range(0, 1 + new Random().nextInt(5)).mapToObj(i -> randomAlphabetic(9)),
Arrays.stream(uuids))
.collect(toSet());
}

}

+ 3
- 2
server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListener.java View File

@@ -19,12 +19,13 @@
*/
package org.sonar.server.project;

import java.util.Set;
import org.sonar.api.server.ServerSide;

@ServerSide
public interface ProjectLifeCycleListener {
/**
* This method is called after the specified project has been deleted.
* This method is called after the specified projects have been deleted.
*/
void onProjectDeleted(Project project);
void onProjectsDeleted(Set<Project> projects);
}

+ 7
- 5
server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListeners.java View File

@@ -19,14 +19,16 @@
*/
package org.sonar.server.project;

import java.util.Set;

public interface ProjectLifeCycleListeners {
/**
* This method is called after the specified project has been deleted and will call method
* {@link ProjectLifeCycleListener#onProjectDeleted(Project) onProjectDeleted(Project)} of all known
* This method is called after the specified projects have been deleted and will call method
* {@link ProjectLifeCycleListener#onProjectsDeleted(Set) onProjectsDeleted(Set)} of all known
* {@link ProjectLifeCycleListener} implementations.
*
* This method will ensure all {@link ProjectLifeCycleListener} implementations are called, even if one or more of
* <p>
* This method ensures all {@link ProjectLifeCycleListener} implementations are called, even if one or more of
* them fail with an exception.
*/
void onProjectDeleted(Project project);
void onProjectsDeleted(Set<Project> projects);
}

+ 10
- 3
server/sonar-server/src/main/java/org/sonar/server/project/ProjectLifeCycleListenersImpl.java View File

@@ -21,6 +21,7 @@ package org.sonar.server.project;

import com.google.common.base.Preconditions;
import java.util.Arrays;
import java.util.Set;
import java.util.function.Consumer;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
@@ -37,16 +38,22 @@ public class ProjectLifeCycleListenersImpl implements ProjectLifeCycleListeners
this.listeners = new ProjectLifeCycleListener[0];
}

/**
* Used by Pico when there is at least one ProjectLifeCycleListener implementation in container.
*/
public ProjectLifeCycleListenersImpl(ProjectLifeCycleListener[] listeners) {
this.listeners = listeners;
}

@Override
public void onProjectDeleted(Project project) {
Preconditions.checkNotNull(project, "project can't be null");
public void onProjectsDeleted(Set<Project> projects) {
Preconditions.checkNotNull(projects, "projects can't be null");
if (projects.isEmpty()) {
return;
}

Arrays.stream(listeners)
.forEach(safelyCallListener(listener -> listener.onProjectDeleted(project)));
.forEach(safelyCallListener(listener -> listener.onProjectsDeleted(projects)));
}

private static Consumer<ProjectLifeCycleListener> safelyCallListener(Consumer<ProjectLifeCycleListener> task) {

+ 13
- 3
server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java View File

@@ -27,12 +27,16 @@ import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.Param;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentQuery;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.permission.OrganizationPermission;
import org.sonar.server.component.ComponentCleanerService;
import org.sonar.server.project.Project;
import org.sonar.server.project.ProjectLifeCycleListeners;
import org.sonar.server.project.Visibility;
import org.sonar.server.user.UserSession;

@@ -61,13 +65,15 @@ public class BulkDeleteAction implements ProjectsWsAction {
private final DbClient dbClient;
private final UserSession userSession;
private final ProjectsWsSupport support;
private final ProjectLifeCycleListeners projectLifeCycleListeners;

public BulkDeleteAction(ComponentCleanerService componentCleanerService, DbClient dbClient, UserSession userSession,
ProjectsWsSupport support) {
ProjectsWsSupport support, ProjectLifeCycleListeners projectLifeCycleListeners) {
this.componentCleanerService = componentCleanerService;
this.dbClient = dbClient;
this.userSession = userSession;
this.support = support;
this.projectLifeCycleListeners = projectLifeCycleListeners;
}

@Override
@@ -139,8 +145,12 @@ public class BulkDeleteAction implements ProjectsWsAction {
userSession.checkPermission(OrganizationPermission.ADMINISTER, organization);

ComponentQuery query = buildDbQuery(searchRequest);
dbClient.componentDao().selectByQuery(dbSession, organization.getUuid(), query, 0, Integer.MAX_VALUE)
.forEach(p -> componentCleanerService.delete(dbSession, p));
List<ComponentDto> componentDtos = dbClient.componentDao().selectByQuery(dbSession, organization.getUuid(), query, 0, Integer.MAX_VALUE);
try {
componentDtos.forEach(p -> componentCleanerService.delete(dbSession, p));
} finally {
projectLifeCycleListeners.onProjectsDeleted(componentDtos.stream().map(Project::from).collect(MoreCollectors.toSet(componentDtos.size())));
}
}
response.noContent();
}

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/project/ws/DeleteAction.java View File

@@ -32,6 +32,7 @@ import org.sonar.server.component.ComponentFinder;
import org.sonar.server.project.ProjectLifeCycleListeners;
import org.sonar.server.user.UserSession;

import static java.util.Collections.singleton;
import static org.sonar.server.component.ComponentFinder.ParamNames.PROJECT_ID_AND_PROJECT;
import static org.sonar.server.project.Project.from;
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
@@ -91,7 +92,7 @@ public class DeleteAction implements ProjectsWsAction {
ComponentDto project = componentFinder.getByUuidOrKey(dbSession, uuid, key, PROJECT_ID_AND_PROJECT);
checkPermission(project);
componentCleanerService.delete(dbSession, project);
projectLifeCycleListeners.onProjectDeleted(from(project));
projectLifeCycleListeners.onProjectsDeleted(singleton(from(project)));
}

response.noContent();

+ 145
- 0
server/sonar-server/src/test/java/org/sonar/server/project/ProjectLifeCycleListenersImplTest.java View File

@@ -0,0 +1,145 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.project;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Collections;
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.mockito.InOrder;
import org.mockito.Mockito;
import org.sonar.core.util.stream.MoreCollectors;

import static java.util.Collections.singleton;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;

@RunWith(DataProviderRunner.class)
public class ProjectLifeCycleListenersImplTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();

private ProjectLifeCycleListener listener1 = mock(ProjectLifeCycleListener.class);
private ProjectLifeCycleListener listener2 = mock(ProjectLifeCycleListener.class);
private ProjectLifeCycleListener listener3 = mock(ProjectLifeCycleListener.class);
private ProjectLifeCycleListenersImpl underTestNoListeners = new ProjectLifeCycleListenersImpl();
private ProjectLifeCycleListenersImpl underTestWithListeners = new ProjectLifeCycleListenersImpl(
new ProjectLifeCycleListener[] {listener1, listener2, listener3});

@Test
public void onProjectsDeleted_throws_NPE_if_set_is_null() {
expectedException.expect(NullPointerException.class);
expectedException.expectMessage("projects can't be null");

underTestWithListeners.onProjectsDeleted(null);
}

@Test
public void onProjectsDeleted_throws_NPE_if_set_is_null_even_if_no_listeners() {
expectedException.expect(NullPointerException.class);
expectedException.expectMessage("projects can't be null");

underTestNoListeners.onProjectsDeleted(null);
}

@Test
public void onProjectsDeleted_has_no_effect_if_set_is_empty() {
underTestNoListeners.onProjectsDeleted(Collections.emptySet());

underTestWithListeners.onProjectsDeleted(Collections.emptySet());
verifyZeroInteractions(listener1, listener2, listener3);
}

@Test
@UseDataProvider("oneOrManyProjects")
public void onProjectsDeleted_does_not_fail_if_there_is_no_listener(Set<Project> projects) {
underTestNoListeners.onProjectsDeleted(projects);
}

@Test
@UseDataProvider("oneOrManyProjects")
public void onProjectsDeleted_calls_all_listeners_in_order_of_addition_to_constructor(Set<Project> projects) {
InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);

underTestWithListeners.onProjectsDeleted(projects);

inOrder.verify(listener1).onProjectsDeleted(same(projects));
inOrder.verify(listener2).onProjectsDeleted(same(projects));
inOrder.verify(listener3).onProjectsDeleted(same(projects));
inOrder.verifyNoMoreInteractions();
}

@Test
@UseDataProvider("oneOrManyProjects")
public void onProjectsDeleted_calls_all_listeners_even_if_one_throws_an_Exception(Set<Project> projects) {
InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
doThrow(new RuntimeException("Faking listener2 throwing an exception"))
.when(listener2)
.onProjectsDeleted(any());

underTestWithListeners.onProjectsDeleted(projects);

inOrder.verify(listener1).onProjectsDeleted(same(projects));
inOrder.verify(listener2).onProjectsDeleted(same(projects));
inOrder.verify(listener3).onProjectsDeleted(same(projects));
inOrder.verifyNoMoreInteractions();
}

@Test
@UseDataProvider("oneOrManyProjects")
public void onProjectsDeleted_calls_all_listeners_even_if_one_throws_an_Error(Set<Project> projects) {
InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
doThrow(new Error("Faking listener2 throwing an Error"))
.when(listener2)
.onProjectsDeleted(any());

underTestWithListeners.onProjectsDeleted(projects);

inOrder.verify(listener1).onProjectsDeleted(same(projects));
inOrder.verify(listener2).onProjectsDeleted(same(projects));
inOrder.verify(listener3).onProjectsDeleted(same(projects));
inOrder.verifyNoMoreInteractions();
}

@DataProvider
public static Object[][] oneOrManyProjects() {
return new Object[][] {
{singleton(newUniqueProject())},
{IntStream.range(0, 1 + new Random().nextInt(10)).mapToObj(i -> newUniqueProject()).collect(MoreCollectors.toSet())}
};
}

private static int counter = 3_989;

private static Project newUniqueProject() {
int base = counter++;
return new Project(base + "_uuid", base + "_key", base + "_name");
}
}

+ 60
- 6
server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java View File

@@ -20,8 +20,11 @@
package org.sonar.server.project.ws;

import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang.StringUtils;
import org.junit.Before;
@@ -43,12 +46,15 @@ import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.organization.BillingValidationsProxy;
import org.sonar.server.organization.TestDefaultOrganizationProvider;
import org.sonar.server.project.Project;
import org.sonar.server.project.ProjectLifeCycleListeners;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.TestResponse;
import org.sonar.server.ws.WsActionTester;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -74,8 +80,9 @@ public class BulkDeleteActionTest {
private ComponentCleanerService componentCleanerService = mock(ComponentCleanerService.class);
private DbClient dbClient = db.getDbClient();
private ProjectsWsSupport support = new ProjectsWsSupport(dbClient, TestDefaultOrganizationProvider.from(db), mock(BillingValidationsProxy.class));
private ProjectLifeCycleListeners projectLifeCycleListeners = mock(ProjectLifeCycleListeners.class);

private BulkDeleteAction underTest = new BulkDeleteAction(componentCleanerService, dbClient, userSession, support);
private BulkDeleteAction underTest = new BulkDeleteAction(componentCleanerService, dbClient, userSession, support, projectLifeCycleListeners);
private WsActionTester ws = new WsActionTester(underTest);

private OrganizationDto org1;
@@ -102,6 +109,7 @@ public class BulkDeleteActionTest {
assertThat(result.getStatus()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT);
assertThat(result.getInput()).isEmpty();
verifyDeleted(toDeleteInOrg2);
verifyListenersOnProjectsDeleted(toDeleteInOrg2);
}

@Test
@@ -117,6 +125,7 @@ public class BulkDeleteActionTest {
.execute();

verifyDeleted(toDeleteInOrg1, toDeleteInOrg2);
verifyListenersOnProjectsDeleted(toDeleteInOrg1, toDeleteInOrg2);
}

@Test
@@ -131,6 +140,7 @@ public class BulkDeleteActionTest {
.execute();

verifyDeleted(toDelete1, toDelete2);
verifyListenersOnProjectsDeleted(toDelete1, toDelete2);
}

@Test
@@ -149,6 +159,7 @@ public class BulkDeleteActionTest {
.execute();

verifyDeleted(oldProject);
verifyListenersOnProjectsDeleted(oldProject);
}

@Test
@@ -161,6 +172,7 @@ public class BulkDeleteActionTest {
ws.newRequest().setParam(PARAM_ON_PROVISIONED_ONLY, "true").execute();

verifyDeleted(provisionedProject);
verifyListenersOnProjectsDeleted(provisionedProject);
}

@Test
@@ -171,6 +183,7 @@ public class BulkDeleteActionTest {
ws.newRequest().execute();

verifyDeleted(projects);
verifyListenersOnProjectsDeleted(projects);
}

@Test
@@ -182,6 +195,7 @@ public class BulkDeleteActionTest {
ws.newRequest().setParam(PARAM_QUALIFIERS, String.join(",", Qualifiers.PROJECT, Qualifiers.VIEW)).execute();

verifyDeleted(project, view);
verifyListenersOnProjectsDeleted(project, view);
}

@Test
@@ -194,6 +208,7 @@ public class BulkDeleteActionTest {
ws.newRequest().setParam(Param.TEXT_QUERY, "JeCt-_%-k").execute();

verifyDeleted(matchKeyProject, matchUppercaseKeyProject);
verifyListenersOnProjectsDeleted(matchKeyProject, matchUppercaseKeyProject);
}

@Test
@@ -204,11 +219,14 @@ public class BulkDeleteActionTest {
expectedException.expect(ForbiddenException.class);
expectedException.expectMessage("Insufficient privileges");

ws.newRequest()
.setParam("projects", project.getDbKey())
.execute();

verifyNoDeletions();
try {
ws.newRequest()
.setParam("projects", project.getDbKey())
.execute();
} finally {
verifyNoDeletions();
verifyZeroInteractions(projectLifeCycleListeners);
}
}

/**
@@ -226,6 +244,33 @@ public class BulkDeleteActionTest {
.execute();

verify(componentCleanerService, times(1_000)).delete(any(DbSession.class), any(ComponentDto.class));
ArgumentCaptor<Set<Project>> projectsCaptor = ArgumentCaptor.forClass(Set.class);
verify(projectLifeCycleListeners).onProjectsDeleted(projectsCaptor.capture());
assertThat(projectsCaptor.getValue()).hasSize(1_000);
}

@Test
public void projectLifeCycleListeners_onProjectsDeleted_called_even_if_delete_fails() {
userSession.logIn().addPermission(ADMINISTER, org1);
ComponentDto project1 = db.components().insertPrivateProject(org1);
ComponentDto project2 = db.components().insertPrivateProject(org1);
ComponentDto project3 = db.components().insertPrivateProject(org1);
ComponentCleanerService componentCleanerService = mock(ComponentCleanerService.class);
RuntimeException expectedException = new RuntimeException("Faking delete failing on 2nd project");
doNothing()
.doThrow(expectedException)
.when(componentCleanerService)
.delete(any(), any(ComponentDto.class));

try {
ws.newRequest()
.setParam("organization", org1.getKey())
.setParam("projects", project1.getDbKey() + "," + project2.getDbKey() + "," + project3.getDbKey())
.execute();
} catch (RuntimeException e) {
assertThat(e).isSameAs(expectedException);
verifyListenersOnProjectsDeleted(project1, project2, project3);
}
}

@Test
@@ -240,6 +285,7 @@ public class BulkDeleteActionTest {
.execute();

verifyDeleted(toDelete);
verifyListenersOnProjectsDeleted(toDelete);
}

@Test
@@ -251,6 +297,7 @@ public class BulkDeleteActionTest {
.setParam("ids", "whatever-the-uuid").execute();

verifyNoDeletions();
verifyZeroInteractions(projectLifeCycleListeners);
}

@Test
@@ -264,6 +311,7 @@ public class BulkDeleteActionTest {
.setParam("ids", "whatever-the-uuid").execute();

verifyNoDeletions();
verifyZeroInteractions(projectLifeCycleListeners);
}

@Test
@@ -279,6 +327,7 @@ public class BulkDeleteActionTest {
.execute();

verifyNoDeletions();
verifyZeroInteractions(projectLifeCycleListeners);
}

private void verifyDeleted(ComponentDto... projects) {
@@ -293,4 +342,9 @@ public class BulkDeleteActionTest {
private void verifyNoDeletions() {
verifyZeroInteractions(componentCleanerService);
}

private void verifyListenersOnProjectsDeleted(ComponentDto... components) {
verify(projectLifeCycleListeners)
.onProjectsDeleted(Arrays.stream(components).map(Project::from).collect(Collectors.toSet()));
}
}

+ 5
- 4
server/sonar-server/src/test/java/org/sonar/server/project/ws/DeleteActionTest.java View File

@@ -45,6 +45,7 @@ import org.sonar.server.project.ProjectLifeCycleListeners;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.WsTester;

import static java.util.Collections.singleton;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -93,7 +94,7 @@ public class DeleteActionTest {
call(request);

assertThat(verifyDeletedKey()).isEqualTo(project.getDbKey());
verify(projectLifeCycleListeners).onProjectDeleted(Project.from(project));
verify(projectLifeCycleListeners).onProjectsDeleted(singleton(Project.from(project)));
}

@Test
@@ -104,7 +105,7 @@ public class DeleteActionTest {
call(newRequest().setParam(PARAM_PROJECT, project.getDbKey()));

assertThat(verifyDeletedKey()).isEqualTo(project.getDbKey());
verify(projectLifeCycleListeners).onProjectDeleted(Project.from(project));
verify(projectLifeCycleListeners).onProjectsDeleted(singleton(Project.from(project)));
}

@Test
@@ -115,7 +116,7 @@ public class DeleteActionTest {
call(newRequest().setParam(PARAM_PROJECT_ID, project.uuid()));

assertThat(verifyDeletedKey()).isEqualTo(project.getDbKey());
verify(projectLifeCycleListeners).onProjectDeleted(Project.from(project));
verify(projectLifeCycleListeners).onProjectsDeleted(singleton(Project.from(project)));
}

@Test
@@ -126,7 +127,7 @@ public class DeleteActionTest {
call(newRequest().setParam(PARAM_PROJECT, project.getDbKey()));

assertThat(verifyDeletedKey()).isEqualTo(project.getDbKey());
verify(projectLifeCycleListeners).onProjectDeleted(Project.from(project));
verify(projectLifeCycleListeners).onProjectsDeleted(singleton(Project.from(project)));
}

@Test

Loading…
Cancel
Save