]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9181 WS api/projects/bulk_delete accepts the sames parameters as api/projects...
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Wed, 6 Sep 2017 10:28:04 +0000 (12:28 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Sep 2017 09:28:29 +0000 (11:28 +0200)
13 files changed:
server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java
server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java
server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java
sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java
sonar-ws/src/test/java/org/sonarqube/ws/client/project/SearchWsRequestTest.java
tests/src/test/java/org/sonarqube/tests/Category6Suite.java
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectAdministrationTest.java
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeleteTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeleteTest.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeletionTest.java [deleted file]

index d7da8b0dc6f830ee723427d26a3dc7f696a27a37..1b756893732f34c1d3ab46282b8e40fa66171c63 100644 (file)
  */
 package org.sonar.server.project.ws;
 
-import java.util.List;
-import java.util.Optional;
-import javax.annotation.Nullable;
 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.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.Visibility;
 import org.sonar.server.user.UserSession;
-
-import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
+import org.sonarqube.ws.client.project.SearchWsRequest;
+
+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.Uuids.UUID_EXAMPLE_01;
+import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02;
+import static org.sonar.server.project.ws.SearchAction.buildDbQuery;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_002;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ON_PROVISIONED_ONLY;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ORGANIZATION;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECTS;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT_IDS;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY;
 
 public class BulkDeleteAction implements ProjectsWsAction {
 
@@ -64,59 +76,79 @@ public class BulkDeleteAction implements ProjectsWsAction {
       .setSince("5.2")
       .setHandler(this);
 
-    action
-      .createParam(PARAM_PROJECT_IDS)
-      .setDescription("List of project IDs to delete")
-      .setDeprecatedKey("ids", "6.4")
-      .setDeprecatedSince("6.4")
-      .setExampleValue("ce4c03d6-430f-40a9-b777-ad877c00aa4d,c526ef20-131b-4486-9357-063fa64b5079");
+    support.addOrganizationParam(action);
 
     action
       .createParam(PARAM_PROJECTS)
-      .setDescription("List of project keys to delete")
+      .setDescription("Comma-separated list of project keys")
       .setDeprecatedKey("keys", "6.4")
-      .setExampleValue(KEY_PROJECT_EXAMPLE_001);
+      .setExampleValue(String.join(",", KEY_PROJECT_EXAMPLE_001, KEY_PROJECT_EXAMPLE_002));
 
-    support.addOrganizationParam(action);
+    action
+      .createParam(PARAM_PROJECT_IDS)
+      .setDescription("Comma-separated list of project ids")
+      .setDeprecatedKey("ids", "6.4")
+      .setDeprecatedSince("6.4")
+      .setExampleValue(String.join(",", UUID_EXAMPLE_01, UUID_EXAMPLE_02));
+
+    action.createParam(Param.TEXT_QUERY)
+      .setDescription("Limit to: <ul>" +
+        "<li>component names that contain the supplied string</li>" +
+        "<li>component keys that contain the supplied string</li>" +
+        "</ul>")
+      .setExampleValue("sonar");
+
+    action.createParam(PARAM_QUALIFIERS)
+      .setDescription("Comma-separated list of component qualifiers. Filter the results with the specified qualifiers")
+      .setPossibleValues(PROJECT, VIEW, APP)
+      .setDefaultValue(PROJECT);
+
+    action.createParam(PARAM_VISIBILITY)
+      .setDescription("Filter the projects that should be visible to everyone (%s), or only specific user/groups (%s).<br/>" +
+        "If no visibility is specified, the default project visibility of the organization will be used.",
+        Visibility.PUBLIC.getLabel(), Visibility.PRIVATE.getLabel())
+      .setRequired(false)
+      .setInternal(true)
+      .setSince("6.4")
+      .setPossibleValues(Visibility.getLabels());
+
+    action.createParam(PARAM_ANALYZED_BEFORE)
+      .setDescription("Filter the projects for which last analysis is older than the given date (exclusive).<br> " +
+        "Format: date or datetime ISO formats.")
+      .setSince("6.6");
+
+    action.createParam(PARAM_ON_PROVISIONED_ONLY)
+      .setDescription("Filter the projects that are provisioned")
+      .setBooleanPossibleValues()
+      .setDefaultValue("false")
+      .setSince("6.6");
   }
 
   @Override
   public void handle(Request request, Response response) throws Exception {
+    SearchWsRequest searchRequest = toSearchWsRequest(request);
     userSession.checkLoggedIn();
-
-    List<String> uuids = request.paramAsStrings(PARAM_PROJECT_IDS);
-    List<String> keys = request.paramAsStrings(PARAM_PROJECTS);
-    String orgKey = request.param(ProjectsWsSupport.PARAM_ORGANIZATION);
-
     try (DbSession dbSession = dbClient.openSession(false)) {
-      Optional<OrganizationDto> org = loadOrganizationByKey(dbSession, orgKey);
-      List<ComponentDto> projects = searchProjects(dbSession, uuids, keys);
-      projects.stream()
-        .filter(p -> !org.isPresent() || org.get().getUuid().equals(p.getOrganizationUuid()))
+      OrganizationDto organization = support.getOrganization(dbSession, searchRequest.getOrganization());
+      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));
     }
-
     response.noContent();
   }
 
-  private Optional<OrganizationDto> loadOrganizationByKey(DbSession dbSession, @Nullable String orgKey) {
-    if (orgKey == null) {
-      userSession.checkIsSystemAdministrator();
-      return Optional.empty();
-    }
-    OrganizationDto org = support.getOrganization(dbSession, orgKey);
-    userSession.checkPermission(ADMINISTER, org);
-    return Optional.of(org);
-  }
-
-  private List<ComponentDto> searchProjects(DbSession dbSession, @Nullable List<String> uuids, @Nullable List<String> keys) {
-    if (uuids != null) {
-      return dbClient.componentDao().selectByUuids(dbSession, uuids);
-    }
-    if (keys != null) {
-      return dbClient.componentDao().selectByKeys(dbSession, keys);
-    }
-
-    throw new IllegalArgumentException("ids or keys must be provided");
+  private static SearchWsRequest toSearchWsRequest(Request request) {
+    return SearchWsRequest.builder()
+      .setOrganization(request.param(PARAM_ORGANIZATION))
+      .setQualifiers(request.mandatoryParamAsStrings(PARAM_QUALIFIERS))
+      .setQuery(request.param(Param.TEXT_QUERY))
+      .setVisibility(request.param(PARAM_VISIBILITY))
+      .setAnalyzedBefore(request.param(PARAM_ANALYZED_BEFORE))
+      .setOnProvisionedOnly(request.mandatoryParamAsBoolean(PARAM_ON_PROVISIONED_ONLY))
+      .setProjects(request.paramAsStrings(PARAM_PROJECTS))
+      .setProjectIds(request.paramAsStrings(PARAM_PROJECT_IDS))
+      .build();
   }
 }
index 7cfd3cb87eeb7e59f6fec8498009466f0df451f2..1be8cde92c1f794056cd2b9ab61b667b19e2f0eb 100644 (file)
@@ -176,7 +176,7 @@ public class SearchAction implements ProjectsWsAction {
     }
   }
 
-  private static ComponentQuery buildDbQuery(SearchWsRequest request) {
+  static ComponentQuery buildDbQuery(SearchWsRequest request) {
     List<String> qualifiers = request.getQualifiers();
     ComponentQuery.Builder query = ComponentQuery.builder()
       .setQualifiers(qualifiers.toArray(new String[qualifiers.size()]));
index 5982f45f5b0560ed1034c000be6cd8b277cd53a5..79f3806677d36701b2a2673af5aa455cd566d6e5 100644 (file)
  */
 package org.sonar.server.project.ws;
 
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.Date;
+import java.util.stream.IntStream;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.mockito.ArgumentCaptor;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.server.ws.WebService.Param;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
@@ -36,7 +42,8 @@ import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.organization.BillingValidationsProxy;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.tester.UserSessionRule;
-import org.sonar.server.ws.WsTester;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.any;
@@ -44,80 +51,149 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.sonar.api.utils.DateUtils.formatDate;
+import static org.sonar.db.component.SnapshotTesting.newAnalysis;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ON_PROVISIONED_ONLY;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ORGANIZATION;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECTS;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
 
 public class BulkDeleteActionTest {
 
-  private static final String ACTION = "bulk_delete";
-
   @Rule
   public DbTester db = DbTester.create(System2.INSTANCE);
-
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
-
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
   private ComponentCleanerService componentCleanerService = mock(ComponentCleanerService.class);
-  private WsTester ws;
   private DbClient dbClient = db.getDbClient();
   private ProjectsWsSupport support = new ProjectsWsSupport(dbClient, TestDefaultOrganizationProvider.from(db), mock(BillingValidationsProxy.class));
+
   private BulkDeleteAction underTest = new BulkDeleteAction(componentCleanerService, dbClient, userSession, support);
+  private WsActionTester ws = new WsActionTester(underTest);
+
   private OrganizationDto org1;
   private OrganizationDto org2;
 
   @Before
   public void setUp() {
-    ws = new WsTester(new ProjectsWs(underTest));
     org1 = db.organizations().insert();
     org2 = db.organizations().insert();
   }
 
   @Test
-  public void system_administrator_deletes_projects_by_uuids_in_all_organizations() throws Exception {
-    userSession.logIn().setSystemAdministrator();
+  public void delete_projects_in_default_organization_if_no_org_provided() throws Exception {
+    userSession.logIn().setRoot();
+    OrganizationDto defaultOrganization = db.getDefaultOrganization();
     ComponentDto toDeleteInOrg1 = db.components().insertPrivateProject(org1);
-    ComponentDto toDeleteInOrg2 = db.components().insertPrivateProject(org2);
-    ComponentDto toKeep = db.components().insertPrivateProject(org2);
+    ComponentDto toDeleteInOrg2 = db.components().insertPrivateProject(defaultOrganization);
+    ComponentDto toKeep = db.components().insertPrivateProject(defaultOrganization);
 
-    WsTester.Result result = ws.newPostRequest("api/projects", ACTION)
-      .setParam("ids", toDeleteInOrg1.uuid() + "," + toDeleteInOrg2.uuid())
+    TestResponse result = ws.newRequest()
+      .setParam("projectIds", toDeleteInOrg1.uuid() + "," + toDeleteInOrg2.uuid())
       .execute();
-    result.assertNoContent();
 
-    verifyDeleted(toDeleteInOrg1, toDeleteInOrg2);
+    assertThat(result.getStatus()).isEqualTo(HttpURLConnection.HTTP_NO_CONTENT);
+    assertThat(result.getInput()).isEmpty();
+    verifyDeleted(toDeleteInOrg2);
   }
 
   @Test
-  public void system_administrator_deletes_projects_by_keys_in_all_organizations() throws Exception {
-    userSession.logIn().setSystemAdministrator();
+  public void delete_projects_by_keys() throws Exception {
+    userSession.logIn().setRoot();
     ComponentDto toDeleteInOrg1 = db.components().insertPrivateProject(org1);
-    ComponentDto toDeleteInOrg2 = db.components().insertPrivateProject(org2);
-    ComponentDto toKeep = db.components().insertPrivateProject(org2);
+    ComponentDto toDeleteInOrg2 = db.components().insertPrivateProject(org1);
+    ComponentDto toKeep = db.components().insertPrivateProject(org1);
 
-    WsTester.Result result = ws.newPostRequest("api/projects", ACTION)
-      .setParam("keys", toDeleteInOrg1.getDbKey() + "," + toDeleteInOrg2.getDbKey())
+    ws.newRequest()
+      .setParam(PARAM_ORGANIZATION, org1.getKey())
+      .setParam(PARAM_PROJECTS, toDeleteInOrg1.getDbKey() + "," + toDeleteInOrg2.getDbKey())
       .execute();
-    result.assertNoContent();
 
     verifyDeleted(toDeleteInOrg1, toDeleteInOrg2);
   }
 
   @Test
   public void projects_that_dont_exist_are_ignored_and_dont_break_bulk_deletion() throws Exception {
-    userSession.logIn().setSystemAdministrator();
+    userSession.logIn().setRoot();
     ComponentDto toDelete1 = db.components().insertPrivateProject(org1);
     ComponentDto toDelete2 = db.components().insertPrivateProject(org1);
 
-    WsTester.Result result = ws.newPostRequest("api/projects", ACTION)
-      .setParam("keys", toDelete1.getDbKey() + ",missing," + toDelete2.getDbKey() + ",doesNotExist")
+    ws.newRequest()
+      .setParam("organization", org1.getKey())
+      .setParam("projects", toDelete1.getDbKey() + ",missing," + toDelete2.getDbKey() + ",doesNotExist")
       .execute();
-    result.assertNoContent();
 
     verifyDeleted(toDelete1, toDelete2);
   }
 
+  @Test
+  public void old_projects() {
+    userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization());
+    long aLongTimeAgo = 1_000_000_000L;
+    long recentTime = 3_000_000_000L;
+    ComponentDto oldProject = db.components().insertPublicProject();
+    db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(oldProject).setCreatedAt(aLongTimeAgo));
+    ComponentDto recentProject = db.components().insertPublicProject();
+    db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(recentProject).setCreatedAt(recentTime));
+    db.commit();
+
+    ws.newRequest()
+      .setParam(PARAM_ANALYZED_BEFORE, formatDate(new Date(recentTime)))
+      .execute();
+
+    verifyDeleted(oldProject);
+  }
+
+  @Test
+  public void provisioned_projects() {
+    userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization());
+    ComponentDto provisionedProject = db.components().insertPrivateProject();
+    ComponentDto analyzedProject = db.components().insertPrivateProject();
+    db.components().insertSnapshot(newAnalysis(analyzedProject));
+
+    ws.newRequest().setParam(PARAM_ON_PROVISIONED_ONLY, "true").execute();
+
+    verifyDeleted(provisionedProject);
+  }
+
+  @Test
+  public void delete_more_than_50_projects() {
+    userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization());
+    ComponentDto[] projects = IntStream.range(0, 55).mapToObj(i -> db.components().insertPrivateProject()).toArray(ComponentDto[]::new);
+
+    ws.newRequest().execute();
+
+    verifyDeleted(projects);
+  }
+
+  @Test
+  public void projects_and_views() throws IOException {
+    userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization());
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto view = db.components().insertView();
+
+    ws.newRequest().setParam(PARAM_QUALIFIERS, String.join(",", Qualifiers.PROJECT, Qualifiers.VIEW)).execute();
+
+    verifyDeleted(project, view);
+  }
+
+  @Test
+  public void delete_by_key_query_with_partial_match_case_insensitive() throws IOException {
+    userSession.logIn().addPermission(ADMINISTER, db.getDefaultOrganization());
+    ComponentDto matchKeyProject = db.components().insertPrivateProject(p -> p.setDbKey("project-_%-key"));
+    ComponentDto matchUppercaseKeyProject = db.components().insertPrivateProject(p -> p.setDbKey("PROJECT-_%-KEY"));
+    ComponentDto noMatchProject = db.components().insertPrivateProject(p -> p.setDbKey("project-key-without-escaped-characters"));
+
+    ws.newRequest().setParam(Param.TEXT_QUERY, "JeCt-_%-k").execute();
+
+    verifyDeleted(matchKeyProject, matchUppercaseKeyProject);
+  }
+
   @Test
   public void throw_ForbiddenException_if_organization_administrator_does_not_set_organization_parameter() throws Exception {
     userSession.logIn().addPermission(ADMINISTER, org1);
@@ -126,8 +202,8 @@ public class BulkDeleteActionTest {
     expectedException.expect(ForbiddenException.class);
     expectedException.expectMessage("Insufficient privileges");
 
-    ws.newPostRequest("api/projects", ACTION)
-      .setParam("keys", project.getDbKey())
+    ws.newRequest()
+      .setParam("projects", project.getDbKey())
       .execute();
 
     verifyNoDeletions();
@@ -139,11 +215,10 @@ public class BulkDeleteActionTest {
     ComponentDto toDelete = db.components().insertPrivateProject(org1);
     ComponentDto cantBeDeleted = db.components().insertPrivateProject(org2);
 
-    WsTester.Result result = ws.newPostRequest("api/projects", ACTION)
+    ws.newRequest()
       .setParam("organization", org1.getKey())
-      .setParam("keys", toDelete.getDbKey() + "," + cantBeDeleted.getDbKey())
+      .setParam("projects", toDelete.getDbKey() + "," + cantBeDeleted.getDbKey())
       .execute();
-    result.assertNoContent();
 
     verifyDeleted(toDelete);
   }
@@ -153,7 +228,7 @@ public class BulkDeleteActionTest {
     expectedException.expect(UnauthorizedException.class);
     expectedException.expectMessage("Authentication is required");
 
-    ws.newPostRequest("api/projects", ACTION)
+    ws.newRequest()
       .setParam("ids", "whatever-the-uuid").execute();
 
     verifyNoDeletions();
@@ -166,7 +241,7 @@ public class BulkDeleteActionTest {
     expectedException.expect(ForbiddenException.class);
     expectedException.expectMessage("Insufficient privileges");
 
-    ws.newPostRequest("api/projects", ACTION)
+    ws.newRequest()
       .setParam("ids", "whatever-the-uuid").execute();
 
     verifyNoDeletions();
@@ -179,7 +254,7 @@ public class BulkDeleteActionTest {
     expectedException.expect(ForbiddenException.class);
     expectedException.expectMessage("Insufficient privileges");
 
-    ws.newPostRequest("api/projects", ACTION)
+    ws.newRequest()
       .setParam("organization", org1.getKey())
       .setParam("ids", "whatever-the-uuid")
       .execute();
index edfd8c7339b98cb35488b3f636c1390de9879f8e..9d5f9e9ce398396d9c0517fee1e1a75725079a33 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonarqube.ws.client;
 
-import com.google.common.base.Joiner;
 import com.google.protobuf.Message;
 import com.google.protobuf.Parser;
 import java.io.InputStream;
@@ -34,8 +33,6 @@ import static com.google.common.base.Strings.isNullOrEmpty;
 
 public abstract class BaseService {
 
-  private static final Joiner MULTI_VALUES_JOINER = Joiner.on(",");
-
   private final WsConnector wsConnector;
   protected final String controller;
 
@@ -71,6 +68,6 @@ public abstract class BaseService {
 
   @CheckForNull
   protected static String inlineMultipleParamValue(@Nullable List<String> values) {
-    return values == null ? null : MULTI_VALUES_JOINER.join(values);
+    return values == null ? null : String.join(",", values);
   }
 }
index 6ebe8424074559cd2f29336a9cec71ceb4565d58..11dc0c4495fed6a7c0c0c2ed23a2e6233a927580 100644 (file)
@@ -85,10 +85,15 @@ public class ProjectsService extends BaseService {
       .setParam("project", request.getKey()));
   }
 
-  public void bulkDelete(BulkDeleteRequest request) {
+  public void bulkDelete(SearchWsRequest request) {
     PostRequest post = new PostRequest(path("bulk_delete"))
-      .setParam("organization", request.getOrganization())
-      .setParam("projects", String.join(",", request.getProjectKeys()));
+      .setParam(PARAM_ORGANIZATION, request.getOrganization())
+      .setParam(PARAM_QUALIFIERS, inlineMultipleParamValue(request.getQualifiers()))
+      .setParam(PARAM_ANALYZED_BEFORE, request.getAnalyzedBefore())
+      .setParam(TEXT_QUERY, request.getQuery())
+      .setParam(PARAM_ON_PROVISIONED_ONLY, request.isOnProvisionedOnly())
+      .setParam(PARAM_PROJECTS, inlineMultipleParamValue(request.getProjects()))
+      .setParam(PARAM_PROJECT_IDS, inlineMultipleParamValue(request.getProjectIds()));
 
     call(post);
   }
@@ -121,8 +126,8 @@ public class ProjectsService extends BaseService {
       .setParam(PAGE, request.getPage())
       .setParam(PAGE_SIZE, request.getPageSize())
       .setParam(PARAM_ON_PROVISIONED_ONLY, request.isOnProvisionedOnly())
-      .setParam(PARAM_PROJECTS, request.getProjects())
-      .setParam(PARAM_PROJECT_IDS, request.getProjectIds());
+      .setParam(PARAM_PROJECTS, inlineMultipleParamValue(request.getProjects()))
+      .setParam(PARAM_PROJECT_IDS, inlineMultipleParamValue(request.getProjectIds()));
     return call(get, SearchWsResponse.parser());
   }
 
index 422bb90f6d9df4cabf50a2fd4e318f4cd05c2a9d..90544b37fc21e46684ab031b79fd453ad08c0589 100644 (file)
  */
 package org.sonarqube.ws.client.project;
 
-import java.util.ArrayList;
 import java.util.List;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
+import org.sonar.api.resources.Qualifiers;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Collections.singletonList;
 import static java.util.Objects.requireNonNull;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.MAX_PAGE_SIZE;
 
@@ -108,7 +109,7 @@ public class SearchWsRequest {
 
   public static class Builder {
     private String organization;
-    private List<String> qualifiers = new ArrayList<>();
+    private List<String> qualifiers = singletonList(Qualifiers.PROJECT);
     private Integer page;
     private Integer pageSize;
     private String query;
index f3727f3885159a1798cd3bfeaf834bc4d599a36f..d1a094f3fcc363db753dfcbdc96a8152fd598cf8 100644 (file)
@@ -125,11 +125,24 @@ public class ProjectsServiceTest {
 
   @Test
   public void bulk_delete() {
-    BulkDeleteRequest request = BulkDeleteRequest.builder().setProjectKeys(Arrays.asList("p1", "p2")).setOrganization("my-org").build();
-    underTest.bulkDelete(request);
+    underTest.bulkDelete(SearchWsRequest.builder()
+      .setOrganization("default")
+      .setQuery("project")
+      .setQualifiers(asList("TRK", "VW"))
+      .setAnalyzedBefore("2017-09-01")
+      .setProjects(Arrays.asList("P1", "P2"))
+      .setOnProvisionedOnly(true)
+      .build());
 
-    assertThat(serviceTester.getPostRequest().getPath()).isEqualTo("api/projects/bulk_delete");
-    assertThat(serviceTester.getPostRequest().getParams()).containsOnly(entry("organization", "my-org"), entry("projects", "p1,p2"));
+    serviceTester.assertThat(serviceTester.getPostRequest())
+      .hasPath("bulk_delete")
+      .hasParam("organization", "default")
+      .hasParam("q", "project")
+      .hasParam("analyzedBefore", "2017-09-01")
+      .hasParam("qualifiers", "TRK,VW")
+      .hasParam("onProvisionedOnly", "true")
+      .hasParam("projects", "P1,P2")
+      .andNoOtherParam();
   }
 
   @Test
index 121a6d5b07515aef79049a53e812a62879de7223..bf0f455215dd3be107c5e7a4fe15d23f7df5ae26 100644 (file)
@@ -50,7 +50,7 @@ public class SearchWsRequestTest {
   }
 
   @Test
-  public void fail_when_page_size_is_greather_then_500() throws Exception {
+  public void fail_when_page_size_is_greater_then_500() throws Exception {
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("Page size must not be greater than 500");
 
index d075f20959660539a238f8409a2e4734b699382d..e1e4e666dc69a1571306da2373bbd68474f3f2d0 100644 (file)
@@ -35,7 +35,7 @@ import org.sonarqube.tests.organization.OrganizationMembershipUiTest;
 import org.sonarqube.tests.organization.OrganizationTest;
 import org.sonarqube.tests.organization.PersonalOrganizationTest;
 import org.sonarqube.tests.organization.RootUserOnOrganizationTest;
-import org.sonarqube.tests.projectAdministration.ProjectDeletionTest;
+import org.sonarqube.tests.projectAdministration.ProjectDeleteTest;
 import org.sonarqube.tests.projectAdministration.ProjectKeyUpdateTest;
 import org.sonarqube.tests.projectAdministration.ProjectProvisioningTest;
 import org.sonarqube.tests.projectAdministration.ProjectSearchTest;
@@ -77,7 +77,7 @@ import static util.ItUtils.xooPlugin;
   LeakProjectsPageTest.class,
   SearchProjectsTest.class,
   RulesWsTest.class,
-  ProjectDeletionTest.class,
+  ProjectDeleteTest.class,
   ProjectProvisioningTest.class,
   ProjectKeyUpdateTest.class,
   ProjectSearchTest.class,
index 3a4d1aad047ccd62de01d0c43d68cf0741ee09d9..b001176c914e4dba116384701b4c9f89cd6c8561 100644 (file)
@@ -47,8 +47,10 @@ import org.sonarqube.ws.WsPermissions;
 import org.sonarqube.ws.client.permission.AddUserToTemplateWsRequest;
 import org.sonarqube.ws.client.permission.CreateTemplateWsRequest;
 import org.sonarqube.ws.client.permission.UsersWsRequest;
+import org.sonarqube.ws.client.project.SearchWsRequest;
 
 import static com.codeborne.selenide.Selenide.$;
+import static java.util.Collections.singletonList;
 import static org.apache.commons.lang.time.DateUtils.addDays;
 import static org.assertj.core.api.Assertions.assertThat;
 import static util.ItUtils.getComponent;
@@ -97,14 +99,15 @@ public class ProjectAdministrationTest {
 
   @Test
   public void fail_when_trying_to_delete_a_file() {
-    expectedException.expect(HttpException.class);
     scanSampleWithDate(ANALYSIS_DATE);
-
     assertThat(getComponent(orchestrator, PROJECT_KEY)).isNotNull();
     assertThat(getComponent(orchestrator, FILE_KEY)).isNotNull();
 
-    // it's forbidden to delete only some files
-    orchestrator.getServer().adminWsClient().post(DELETE_WS_ENDPOINT, "keys", FILE_KEY);
+    expectedException.expect(org.sonarqube.ws.client.HttpException.class);
+
+    tester.wsClient().projects().bulkDelete(SearchWsRequest.builder()
+      .setQualifiers(singletonList("FIL"))
+      .setProjects(singletonList(FILE_KEY)).build());
   }
 
   @Test
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeleteTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeleteTest.java
new file mode 100644 (file)
index 0000000..28d601e
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.util.List;
+import java.util.stream.IntStream;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Category6Suite;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsProjects.CreateWsResponse;
+import org.sonarqube.ws.WsProjects.SearchWsResponse.Component;
+import org.sonarqube.ws.client.project.SearchWsRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.runProjectAnalysis;
+
+public class ProjectBulkDeleteTest {
+
+  @ClassRule
+  public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+  @Rule
+  public Tester tester = new Tester(orchestrator);
+
+  @Test
+  public void delete_projects() {
+    Organizations.Organization organization = tester.organizations().generate();
+    CreateWsResponse.Project firstProvisionedProject = tester.projects().generate(organization, p -> p.setKey("first-provisioned-project"));
+    CreateWsResponse.Project secondProvisionedProject = tester.projects().generate(organization, p -> p.setKey("second-provisioned-project"));
+    CreateWsResponse.Project analyzedProject = tester.projects().generate(organization);
+
+    analyzeProject(analyzedProject.getKey(), organization.getKey());
+
+    tester.wsClient().projects().bulkDelete(SearchWsRequest.builder()
+      .setOrganization(organization.getKey())
+      .setQuery("FIRST-PROVISIONED")
+      .setOnProvisionedOnly(true).build());
+
+    List<Component> projects = tester.wsClient().projects().search(SearchWsRequest.builder().setOrganization(organization.getKey()).build()).getComponentsList();
+    assertThat(projects).extracting(Component::getKey)
+      .containsExactlyInAnyOrder(analyzedProject.getKey(), secondProvisionedProject.getKey())
+      .doesNotContain(firstProvisionedProject.getKey());
+  }
+
+  @Test
+  public void delete_more_than_50_projects_at_the_same_time() {
+    Organizations.Organization organization = tester.organizations().generate();
+    IntStream.range(0, 60).forEach(i -> tester.projects().generate(organization));
+    SearchWsRequest request = SearchWsRequest.builder().setOrganization(organization.getKey()).build();
+    assertThat(tester.wsClient().projects().search(request).getPaging().getTotal()).isEqualTo(60);
+
+    tester.wsClient().projects().bulkDelete(request);
+
+    assertThat(tester.wsClient().projects().search(request).getComponentsList()).isEmpty();
+    assertThat(tester.wsClient().projects().search(request).getPaging().getTotal()).isEqualTo(0);
+  }
+
+  private void analyzeProject(String projectKey, String organizationKey) {
+    runProjectAnalysis(orchestrator, "shared/xoo-sample",
+      "sonar.organization", organizationKey,
+      "sonar.projectKey", projectKey,
+      "sonar.login", "admin",
+      "sonar.password", "admin");
+  }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeleteTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeleteTest.java
new file mode 100644 (file)
index 0000000..b0b4310
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonarqube.tests.Category6Suite;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsComponents;
+import org.sonarqube.ws.WsProjects;
+import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.component.SearchProjectsRequest;
+import org.sonarqube.ws.client.project.CreateRequest;
+import org.sonarqube.ws.client.project.DeleteRequest;
+import org.sonarqube.ws.client.project.SearchWsRequest;
+import util.ItUtils;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProjectDeleteTest {
+
+  @ClassRule
+  public static final Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+
+  @Rule
+  public TestRule safeguard = new DisableOnDebug(Timeout.seconds(300));
+  @Rule
+  public Tester tester = new Tester(orchestrator)
+    .setElasticsearchHttpPort(Category6Suite.SEARCH_HTTP_PORT);
+
+  @Test
+  public void deletion_removes_project_from_search_engines() {
+    Organizations.Organization organization = tester.organizations().generate();
+    Project project1 = createProject(organization, "one", "Foo");
+    Project project2 = createProject(organization, "two", "Bar");
+    assertThatProjectIsSearchable(organization, "Foo");
+    assertThatProjectIsSearchable(organization, "Bar");
+
+    deleteProject(project1);
+    assertThatProjectIsNotSearchable(organization, project1.getName());
+    assertThatProjectIsSearchable(organization, project2.getName());
+
+    deleteProject(project2);
+    assertThatProjectIsNotSearchable(organization, project1.getName());
+    assertThatProjectIsNotSearchable(organization, project2.getName());
+  }
+
+  @Test
+  public void indexing_errors_are_recovered_asynchronously_when_deleting_project() throws Exception {
+    Organizations.Organization organization = tester.organizations().generate();
+    Project project = createProject(organization, "one", "Foo");
+
+    tester.elasticsearch().lockWrites("components");
+    tester.elasticsearch().lockWrites("projectmeasures");
+    deleteProject(project);
+    // WS reloads from database the results returned by Elasticsearch. That's
+    // why the project does not appear in search engine.
+    // However this test is still useful to verify that WS do not
+    // fail during this Elasticsearch inconsistency.
+    assertThatProjectIsNotSearchable(organization, project.getName());
+
+    tester.elasticsearch().unlockWrites("components");
+    tester.elasticsearch().unlockWrites("projectmeasures");
+    // TODO verify that recovery daemon successfully updated indices
+  }
+
+  @Test
+  public void bulk_deletion_removes_projects_from_search_engines() {
+    Organizations.Organization organization = tester.organizations().generate();
+    Project project1 = createProject(organization, "one", "Foo");
+    Project project2 = createProject(organization, "two", "Bar");
+    Project project3 = createProject(organization, "three", "Baz");
+
+    bulkDeleteProjects(organization, project1, project3);
+    assertThatProjectIsNotSearchable(organization, project1.getName());
+    assertThatProjectIsSearchable(organization, project2.getName());
+    assertThatProjectIsNotSearchable(organization, project3.getName());
+  }
+
+  @Test
+  public void indexing_errors_are_recovered_asynchronously_when_bulk_deleting_projects() throws Exception {
+    Organizations.Organization organization = tester.organizations().generate();
+    Project project1 = createProject(organization, "one", "Foo");
+    Project project2 = createProject(organization, "two", "Bar");
+    Project project3 = createProject(organization, "three", "Baz");
+
+    tester.elasticsearch().lockWrites("components");
+    tester.elasticsearch().lockWrites("projectmeasures");
+    bulkDeleteProjects(organization, project1, project3);
+
+    // WS reloads from database the results returned by Elasticsearch. That's
+    // why the project does not appear in search engine.
+    // However this test is still useful to verify that WS do not
+    // fail during this Elasticsearch inconsistency.
+    assertThatProjectIsNotSearchable(organization, project1.getName());
+    assertThatProjectIsSearchable(organization, project2.getName());
+    assertThatProjectIsNotSearchable(organization, project3.getName());
+
+    tester.elasticsearch().unlockWrites("components");
+    tester.elasticsearch().unlockWrites("projectmeasures");
+    // TODO verify that recovery daemon successfully updated indices
+  }
+
+  private void deleteProject(Project project) {
+    tester.wsClient().projects().delete(DeleteRequest.builder().setKey(project.getKey()).build());
+  }
+
+  private void bulkDeleteProjects(Organizations.Organization organization, Project... projects) {
+    SearchWsRequest request = SearchWsRequest.builder()
+      .setOrganization(organization.getKey())
+      .setProjects(Arrays.stream(projects).map(Project::getKey).collect(Collectors.toList()))
+      .build();
+    tester.wsClient().projects().bulkDelete(request);
+  }
+
+  private Project createProject(Organizations.Organization organization, String key, String name) {
+    CreateRequest createRequest = CreateRequest.builder().setKey(key).setName(name).setOrganization(organization.getKey()).build();
+    return tester.wsClient().projects().create(createRequest).getProject();
+  }
+
+  private void assertThatProjectIsSearchable(Organizations.Organization organization, String name) {
+    assertThat(isInProjectsSearch(organization, name)).isTrue();
+    assertThat(isInComponentSearchProjects(name)).isTrue();
+    assertThat(isInComponentSuggestions(name)).isTrue();
+  }
+
+  private void assertThatProjectIsNotSearchable(Organizations.Organization organization, String name) {
+    assertThat(isInProjectsSearch(organization, name)).isFalse();
+    assertThat(isInComponentSearchProjects(name)).isFalse();
+    assertThat(isInComponentSuggestions(name)).isFalse();
+  }
+
+  /**
+   * Projects administration page - uses database
+   */
+  private boolean isInProjectsSearch(Organizations.Organization organization, String name) {
+    WsProjects.SearchWsResponse response = tester.wsClient().projects().search(
+      SearchWsRequest.builder().setOrganization(organization.getKey()).setQuery(name).setQualifiers(singletonList("TRK")).build());
+    return response.getComponentsCount() > 0;
+  }
+
+  /**
+   * Projects page - api/components/search_projects - uses ES + DB
+   */
+  private boolean isInComponentSearchProjects(String name) {
+    WsComponents.SearchProjectsWsResponse response = tester.wsClient().components().searchProjects(
+      SearchProjectsRequest.builder().setFilter("query=\"" + name + "\"").build());
+    return response.getComponentsCount() > 0;
+  }
+
+  /**
+   * Top-right search engine - api/components/suggestions - uses ES + DB
+   */
+  private boolean isInComponentSuggestions(String name) {
+    GetRequest request = new GetRequest("api/components/suggestions").setParam("s", name);
+    WsResponse response = tester.wsClient().wsConnector().call(request);
+    Map<String, Object> json = ItUtils.jsonToMap(response.content());
+    Collection<Map<String, Object>> results = (Collection<Map<String, Object>>) json.get("results");
+    Collection items = results.stream()
+      .filter(map -> "TRK".equals(map.get("q")))
+      .map(map -> (Collection) map.get("items"))
+      .findFirst()
+      .orElseThrow(() -> new IllegalStateException("missing field results/[q=TRK]"));
+    return !items.isEmpty();
+  }
+}
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeletionTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectDeletionTest.java
deleted file mode 100644 (file)
index d9ea32b..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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.sonarqube.tests.projectAdministration;
-
-import com.sonar.orchestrator.Orchestrator;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Map;
-import java.util.stream.Collectors;
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.DisableOnDebug;
-import org.junit.rules.TestRule;
-import org.junit.rules.Timeout;
-import org.sonarqube.tests.Category6Suite;
-import org.sonarqube.tests.Tester;
-import org.sonarqube.ws.Organizations;
-import org.sonarqube.ws.WsComponents;
-import org.sonarqube.ws.WsProjects;
-import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
-import org.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.WsResponse;
-import org.sonarqube.ws.client.component.SearchProjectsRequest;
-import org.sonarqube.ws.client.project.BulkDeleteRequest;
-import org.sonarqube.ws.client.project.CreateRequest;
-import org.sonarqube.ws.client.project.DeleteRequest;
-import org.sonarqube.ws.client.project.SearchWsRequest;
-import util.ItUtils;
-
-import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ProjectDeletionTest {
-
-  @ClassRule
-  public static final Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
-
-  @Rule
-  public TestRule safeguard = new DisableOnDebug(Timeout.seconds(300));
-  @Rule
-  public Tester tester = new Tester(orchestrator)
-    .setElasticsearchHttpPort(Category6Suite.SEARCH_HTTP_PORT);
-
-  @Test
-  public void deletion_removes_project_from_search_engines() {
-    Organizations.Organization organization = tester.organizations().generate();
-    Project project1 = createProject(organization, "one", "Foo");
-    Project project2 = createProject(organization, "two", "Bar");
-    assertThatProjectIsSearchable(organization, "Foo");
-    assertThatProjectIsSearchable(organization, "Bar");
-
-    deleteProject(project1);
-    assertThatProjectIsNotSearchable(organization, project1.getName());
-    assertThatProjectIsSearchable(organization, project2.getName());
-
-    deleteProject(project2);
-    assertThatProjectIsNotSearchable(organization, project1.getName());
-    assertThatProjectIsNotSearchable(organization, project2.getName());
-  }
-
-  @Test
-  public void indexing_errors_are_recovered_asynchronously_when_deleting_project() throws Exception {
-    Organizations.Organization organization = tester.organizations().generate();
-    Project project = createProject(organization, "one", "Foo");
-
-    tester.elasticsearch().lockWrites("components");
-    tester.elasticsearch().lockWrites("projectmeasures");
-    deleteProject(project);
-    // WS reloads from database the results returned by Elasticsearch. That's
-    // why the project does not appear in search engine.
-    // However this test is still useful to verify that WS do not
-    // fail during this Elasticsearch inconsistency.
-    assertThatProjectIsNotSearchable(organization, project.getName());
-
-    tester.elasticsearch().unlockWrites("components");
-    tester.elasticsearch().unlockWrites("projectmeasures");
-    // TODO verify that recovery daemon successfully updated indices
-  }
-
-  @Test
-  public void bulk_deletion_removes_projects_from_search_engines() {
-    Organizations.Organization organization = tester.organizations().generate();
-    Project project1 = createProject(organization, "one", "Foo");
-    Project project2 = createProject(organization, "two", "Bar");
-    Project project3 = createProject(organization, "three", "Baz");
-
-    bulkDeleteProjects(organization, project1, project3);
-    assertThatProjectIsNotSearchable(organization, project1.getName());
-    assertThatProjectIsSearchable(organization, project2.getName());
-    assertThatProjectIsNotSearchable(organization, project3.getName());
-  }
-
-  @Test
-  public void indexing_errors_are_recovered_asynchronously_when_bulk_deleting_projects() throws Exception {
-    Organizations.Organization organization = tester.organizations().generate();
-    Project project1 = createProject(organization, "one", "Foo");
-    Project project2 = createProject(organization, "two", "Bar");
-    Project project3 = createProject(organization, "three", "Baz");
-
-    tester.elasticsearch().lockWrites("components");
-    tester.elasticsearch().lockWrites("projectmeasures");
-    bulkDeleteProjects(organization, project1, project3);
-
-    // WS reloads from database the results returned by Elasticsearch. That's
-    // why the project does not appear in search engine.
-    // However this test is still useful to verify that WS do not
-    // fail during this Elasticsearch inconsistency.
-    assertThatProjectIsNotSearchable(organization, project1.getName());
-    assertThatProjectIsSearchable(organization, project2.getName());
-    assertThatProjectIsNotSearchable(organization, project3.getName());
-
-    tester.elasticsearch().unlockWrites("components");
-    tester.elasticsearch().unlockWrites("projectmeasures");
-    // TODO verify that recovery daemon successfully updated indices
-  }
-
-  private void deleteProject(Project project) {
-    tester.wsClient().projects().delete(DeleteRequest.builder().setKey(project.getKey()).build());
-  }
-
-  private void bulkDeleteProjects(Organizations.Organization organization, Project... projects) {
-    BulkDeleteRequest request = BulkDeleteRequest.builder()
-      .setOrganization(organization.getKey())
-      .setProjectKeys(Arrays.stream(projects).map(Project::getKey).collect(Collectors.toList()))
-      .build();
-    tester.wsClient().projects().bulkDelete(request);
-  }
-
-  private Project createProject(Organizations.Organization organization, String key, String name) {
-    CreateRequest createRequest = CreateRequest.builder().setKey(key).setName(name).setOrganization(organization.getKey()).build();
-    return tester.wsClient().projects().create(createRequest).getProject();
-  }
-
-  private void assertThatProjectIsSearchable(Organizations.Organization organization, String name) {
-    assertThat(isInProjectsSearch(organization, name)).isTrue();
-    assertThat(isInComponentSearchProjects(name)).isTrue();
-    assertThat(isInComponentSuggestions(name)).isTrue();
-  }
-
-  private void assertThatProjectIsNotSearchable(Organizations.Organization organization, String name) {
-    assertThat(isInProjectsSearch(organization, name)).isFalse();
-    assertThat(isInComponentSearchProjects(name)).isFalse();
-    assertThat(isInComponentSuggestions(name)).isFalse();
-  }
-
-  /**
-   * Projects administration page - uses database
-   */
-  private boolean isInProjectsSearch(Organizations.Organization organization, String name) {
-    WsProjects.SearchWsResponse response = tester.wsClient().projects().search(
-      SearchWsRequest.builder().setOrganization(organization.getKey()).setQuery(name).setQualifiers(singletonList("TRK")).build());
-    return response.getComponentsCount() > 0;
-  }
-
-  /**
-   * Projects page - api/components/search_projects - uses ES + DB
-   */
-  private boolean isInComponentSearchProjects(String name) {
-    WsComponents.SearchProjectsWsResponse response = tester.wsClient().components().searchProjects(
-      SearchProjectsRequest.builder().setFilter("query=\"" + name + "\"").build());
-    return response.getComponentsCount() > 0;
-  }
-
-  /**
-   * Top-right search engine - api/components/suggestions - uses ES + DB
-   */
-  private boolean isInComponentSuggestions(String name) {
-    GetRequest request = new GetRequest("api/components/suggestions").setParam("s", name);
-    WsResponse response = tester.wsClient().wsConnector().call(request);
-    Map<String, Object> json = ItUtils.jsonToMap(response.content());
-    Collection<Map<String, Object>> results = (Collection<Map<String, Object>>) json.get("results");
-    Collection items = results.stream()
-      .filter(map -> "TRK".equals(map.get("q")))
-      .map(map -> (Collection) map.get("items"))
-      .findFirst()
-      .orElseThrow(() -> new IllegalStateException("missing field results/[q=TRK]"));
-    return !items.isEmpty();
-  }
-}