]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10511 Project key renaming should rename deleted components too
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Thu, 29 Mar 2018 15:09:18 +0000 (17:09 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 5 Apr 2018 18:20:48 +0000 (20:20 +0200)
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentKeyUpdaterDao.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentKeyUpdaterMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentKeyUpdaterDaoTest.java
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/ProjectTester.java
server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ComponentServiceUpdateKeyTest.java
tests/src/test/java/org/sonarqube/tests/project/ProjectKeyUpdateTest.java

index a83a1849105c84e4972c3f253b46d5acb48f22a2..6a0ea848516eeb3f2157682ca3d7ff25b27c0291 100644 (file)
@@ -83,7 +83,7 @@ public class ComponentKeyUpdaterDao implements Dao {
    * @return a map with currentKey/newKey is a bulk update was executed
    */
   public Map<String, String> simulateBulkUpdateKey(DbSession dbSession, String projectUuid, String stringToReplace, String replacementString) {
-    return collectAllModules(projectUuid, stringToReplace, mapper(dbSession))
+    return collectAllModules(projectUuid, stringToReplace, mapper(dbSession), false)
       .stream()
       .collect(Collectors.toMap(
         ResourceDto::getKey,
@@ -109,7 +109,7 @@ public class ComponentKeyUpdaterDao implements Dao {
   public void bulkUpdateKey(DbSession session, String projectUuid, String stringToReplace, String replacementString) {
     ComponentKeyUpdaterMapper mapper = session.getMapper(ComponentKeyUpdaterMapper.class);
     // must SELECT first everything
-    Set<ResourceDto> modules = collectAllModules(projectUuid, stringToReplace, mapper);
+    Set<ResourceDto> modules = collectAllModules(projectUuid, stringToReplace, mapper, true);
     checkNewNameOfAllModules(modules, stringToReplace, replacementString, mapper);
 
     // add branches (no check should be done as branch keys cannot be changed by the user)
@@ -118,7 +118,7 @@ public class ComponentKeyUpdaterDao implements Dao {
       .stream()
       .filter(branch -> !projectUuid.equals(branch.getUuid()))
       .forEach(branch -> {
-        Set<ResourceDto> branchModules = collectAllModules(branch.getUuid(), stringToReplace, mapper);
+        Set<ResourceDto> branchModules = collectAllModules(branch.getUuid(), stringToReplace, mapper, true);
         modules.addAll(branchModules);
         branchModules.forEach(module -> branchBaseKeys.put(module.getKey(), branchBaseKey(module.getKey())));
       });
@@ -165,14 +165,14 @@ public class ComponentKeyUpdaterDao implements Dao {
     }
   }
 
-  private static Set<ResourceDto> collectAllModules(String projectUuid, String stringToReplace, ComponentKeyUpdaterMapper mapper) {
+  private static Set<ResourceDto> collectAllModules(String projectUuid, String stringToReplace, ComponentKeyUpdaterMapper mapper, boolean includeDisabled) {
     ResourceDto project = mapper.selectProject(projectUuid);
     Set<ResourceDto> modules = new HashSet<>();
-    if (project.getKey().contains(stringToReplace)) {
+    if (project.getKey().contains(stringToReplace) && (project.isEnabled() || includeDisabled)) {
       modules.add(project);
     }
     for (ResourceDto submodule : mapper.selectDescendantProjects(projectUuid)) {
-      modules.addAll(collectAllModules(submodule.getUuid(), stringToReplace, mapper));
+      modules.addAll(collectAllModules(submodule.getUuid(), stringToReplace, mapper, includeDisabled));
     }
     return modules;
   }
index d7189fdb1ec65e96fd36eb424e91173b0d5a93d9..51f2114ab66c442a51e4cc68798dab4db28d1717 100644 (file)
@@ -29,7 +29,6 @@
     where
     root_uuid = #{rootUuid,jdbcType=VARCHAR}
     and scope != 'PRJ'
-    and enabled = ${_true}
   </select>
 
   <select id="selectDescendantProjects" parameterType="String" resultMap="resourceResultMap">
@@ -38,7 +37,6 @@
     scope='PRJ'
     and root_uuid = #{rootUuid,jdbcType=VARCHAR}
     and uuid != #{rootUuid,jdbcType=VARCHAR}
-    and enabled = ${_true}
   </select>
 
   <update id="update" parameterType="Resource">
index 2e39482e656dcd731f0d6a815d4a3243918a4720..b7f4e9cf381852ed7f57dc163c9cc3726149df7d 100644 (file)
@@ -63,7 +63,7 @@ public class ComponentKeyUpdaterDaoTest {
   }
 
   @Test
-  public void updateKey_does_not_update_inactive_components() {
+  public void updateKey_updates_disabled_components() {
     OrganizationDto organizationDto = db.organizations().insert();
     ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organizationDto, "A").setDbKey("my_project"));
     ComponentDto directory = db.components().insertComponent(newDirectory(project, "/directory").setDbKey("my_project:directory"));
@@ -75,8 +75,10 @@ public class ComponentKeyUpdaterDaoTest {
     dbSession.commit();
 
     List<ComponentDto> result = dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, "your_project");
-    assertThat(result).hasSize(5).extracting(ComponentDto::getDbKey)
-      .containsOnlyOnce("your_project", "your_project:directory", "your_project:directory/file", "my_project:inactive_directory", "my_project:inactive_directory/file");
+    assertThat(result)
+      .hasSize(5)
+      .extracting(ComponentDto::getDbKey)
+      .containsOnlyOnce("your_project", "your_project:directory", "your_project:directory/file", "your_project:inactive_directory", "your_project:inactive_directory/file");
   }
 
   @Test
@@ -216,7 +218,7 @@ public class ComponentKeyUpdaterDaoTest {
   }
 
   @Test
-  public void bulk_update_key_does_not_update_inactive_components() {
+  public void bulk_update_key_updates_disabled_components() {
     ComponentDto project = db.components().insertComponent(newPrivateProjectDto(db.getDefaultOrganization(), "A").setDbKey("my_project"));
     db.components().insertComponent(newModuleDto(project).setDbKey("my_project:module"));
     db.components().insertComponent(newModuleDto(project).setDbKey("my_project:inactive_module").setEnabled(false));
@@ -224,8 +226,10 @@ public class ComponentKeyUpdaterDaoTest {
     underTest.bulkUpdateKey(dbSession, "A", "my_", "your_");
 
     List<ComponentDto> result = dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, "your_project");
-    assertThat(result).hasSize(3).extracting(ComponentDto::getDbKey)
-      .containsOnlyOnce("your_project", "your_project:module", "my_project:inactive_module");
+    assertThat(result)
+      .hasSize(3)
+      .extracting(ComponentDto::getDbKey)
+      .containsOnlyOnce("your_project", "your_project:module", "your_project:inactive_module");
   }
 
   @Test
@@ -328,16 +332,17 @@ public class ComponentKeyUpdaterDaoTest {
   }
 
   @Test
-  public void simulate_bulk_update_key_do_not_return_disable_components() {
+  public void simulate_bulk_update_key_does_not_return_disable_components() {
     ComponentDto project = db.components().insertComponent(newPrivateProjectDto(db.getDefaultOrganization(), "A").setDbKey("project"));
     db.components().insertComponent(newModuleDto(project).setDbKey("project:enabled-module"));
     db.components().insertComponent(newModuleDto(project).setDbKey("project:disabled-module").setEnabled(false));
+    db.components().insertComponent(newPrivateProjectDto(db.getDefaultOrganization(), "D").setDbKey("other-project"));
 
     Map<String, String> result = underTest.simulateBulkUpdateKey(dbSession, "A", "project", "new-project");
 
-    assertThat(result)
-      .hasSize(2)
-      .containsOnly(entry("project", "new-project"), entry("project:enabled-module", "new-project:enabled-module"));
+    assertThat(result).containsOnly(
+      entry("project", "new-project"),
+      entry("project:enabled-module", "new-project:enabled-module"));
   }
 
   @Test
index 6cf912f2f32a0bcf03cf25fd9c0de64bebb7e52c..21443871a10cae7b67cdea99b13cd14c1c077b10 100644 (file)
@@ -22,8 +22,11 @@ package org.sonarqube.qa.util;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import javax.annotation.Nullable;
+import org.sonarqube.ws.Components;
 import org.sonarqube.ws.Organizations;
 import org.sonarqube.ws.Projects;
+import org.sonarqube.ws.client.HttpException;
+import org.sonarqube.ws.client.components.ShowRequest;
 import org.sonarqube.ws.client.projects.CreateRequest;
 import org.sonarqube.ws.client.projects.DeleteRequest;
 import org.sonarqube.ws.client.projects.ProjectsService;
@@ -64,4 +67,16 @@ public class ProjectTester {
 
     return session.wsClient().projects().create(request).getProject();
   }
+
+  public boolean exists(String projectKey) {
+    try {
+      Components.ShowWsResponse response = session.wsClient().components().show(new ShowRequest().setComponent(projectKey));
+      return response.getComponent() != null;
+    } catch (HttpException e) {
+      if (e.code() == 404) {
+        return false;
+      }
+      throw new IllegalStateException(e);
+    }
+  }
 }
index b1162d21da903d605789d243bedd78d4c1ac84e9..bd5c5740a3a8df96954009d5ffe29fe658aa7575 100644 (file)
@@ -65,8 +65,8 @@ public class ComponentServiceTest {
     assertComponentKeyUpdated(project.getDbKey(), "your_project");
     assertComponentKeyUpdated(module.getDbKey(), "your_project:root:module");
     assertComponentKeyUpdated(file.getDbKey(), "your_project:root:module:src/File.xoo");
-    assertComponentKeyNotUpdated(inactiveModule.getDbKey());
-    assertComponentKeyNotUpdated(inactiveFile.getDbKey());
+    assertComponentKeyUpdated(inactiveModule.getDbKey(), "your_project:root:inactive_module");
+    assertComponentKeyUpdated(inactiveFile.getDbKey(), "your_project:root:module:src/InactiveFile.xoo");
   }
 
   private void assertComponentKeyUpdated(String oldKey, String newKey) {
index 0d5b8a0a0196a9923f26cb15dfb852fa12dc3bc0..f8932006235f3f6166a35e17d1869ef93d85ba4d 100644 (file)
@@ -76,8 +76,9 @@ public class ComponentServiceUpdateKeyTest {
     // Check file key has been updated
     assertThat(db.getDbClient().componentDao().selectByKey(dbSession, file.getDbKey())).isAbsent();
     assertThat(db.getDbClient().componentDao().selectByKey(dbSession, "sample2:root:src/File.xoo")).isNotNull();
+    assertThat(db.getDbClient().componentDao().selectByKey(dbSession, "sample2:root:src/InactiveFile.xoo")).isNotNull();
 
-    assertThat(dbClient.componentDao().selectByKey(dbSession, inactiveFile.getDbKey())).isPresent();
+    assertThat(dbClient.componentDao().selectByKey(dbSession, inactiveFile.getDbKey())).isAbsent();
 
     org.assertj.core.api.Assertions.assertThat(projectIndexers.hasBeenCalled(project.uuid(), ProjectIndexer.Cause.PROJECT_KEY_UPDATE)).isTrue();
   }
@@ -185,8 +186,8 @@ public class ComponentServiceUpdateKeyTest {
     assertComponentKeyUpdated(project.getDbKey(), "your_project");
     assertComponentKeyUpdated(module.getDbKey(), "your_project:root:module");
     assertComponentKeyUpdated(file.getDbKey(), "your_project:root:module:src/File.xoo");
-    assertComponentKeyNotUpdated(inactiveModule.getDbKey());
-    assertComponentKeyNotUpdated(inactiveFile.getDbKey());
+    assertComponentKeyUpdated(inactiveModule.getDbKey(), "your_project:root:inactive_module");
+    assertComponentKeyUpdated(inactiveFile.getDbKey(), "your_project:root:module:src/InactiveFile.xoo");
   }
 
   private void assertComponentKeyUpdated(String oldKey, String newKey) {
@@ -194,10 +195,6 @@ public class ComponentServiceUpdateKeyTest {
     assertThat(dbClient.componentDao().selectByKey(dbSession, newKey)).isPresent();
   }
 
-  private void assertComponentKeyNotUpdated(String key) {
-    assertThat(dbClient.componentDao().selectByKey(dbSession, key)).isPresent();
-  }
-
   private ComponentDto insertSampleRootProject() {
     return insertProject("sample:root");
   }
index f77c1d4fb9d035fb18e9321adede042e38857dbf..d4f004dd9f14327c62ceece94eec89f1b613c051 100644 (file)
@@ -21,31 +21,43 @@ package org.sonarqube.tests.project;
 
 import com.sonar.orchestrator.Orchestrator;
 import com.sonar.orchestrator.build.SonarScanner;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 import java.util.stream.Collectors;
 import javax.annotation.CheckForNull;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
 import org.junit.After;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TemporaryFolder;
 import org.junit.rules.TestRule;
 import org.junit.rules.Timeout;
 import org.sonarqube.qa.util.Tester;
 import org.sonarqube.ws.Components;
 import org.sonarqube.ws.Organizations;
 import org.sonarqube.ws.Projects;
+import org.sonarqube.ws.Projects.BulkUpdateKeyWsResponse.Key;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.WsResponse;
 import org.sonarqube.ws.client.components.SearchProjectsRequest;
 import org.sonarqube.ws.client.components.ShowRequest;
+import org.sonarqube.ws.client.components.TreeRequest;
+import org.sonarqube.ws.client.projects.BulkUpdateKeyRequest;
 import org.sonarqube.ws.client.projects.UpdateKeyRequest;
-import org.sonarqube.ws.client.projects.CreateRequest;
 import util.ItUtils;
 
+import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
 import static util.ItUtils.projectDir;
 
 public class ProjectKeyUpdateTest {
@@ -57,7 +69,8 @@ public class ProjectKeyUpdateTest {
 
   @Rule
   public TestRule safeguard = new DisableOnDebug(Timeout.seconds(300));
-
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
   @Rule
   public Tester tester = new Tester(orchestrator).setElasticsearchHttpPort(ProjectSuite.SEARCH_HTTP_PORT);
 
@@ -67,17 +80,18 @@ public class ProjectKeyUpdateTest {
   }
 
   @Test
-  public void update_key() {
-    analyzeXooSample();
-    String newProjectKey = "another_project_key";
-    Components.Component project = tester.wsClient().components().show(new ShowRequest().setComponent(PROJECT_KEY)).getComponent();
-    assertThat(project.getKey()).isEqualTo(PROJECT_KEY);
+  public void update_key() throws IOException {
+    Organizations.Organization organization = tester.organizations().generate();
+    File projectDir = new XooProjectBuilder(PROJECT_KEY)
+      .build(temp.newFolder());
+    analyze(organization, projectDir);
+    assertThat(tester.projects().exists(PROJECT_KEY)).isTrue();
 
-    tester.wsClient().projects().updateKey(new UpdateKeyRequest()
-      .setFrom(PROJECT_KEY)
-      .setTo(newProjectKey));
+    String newProjectKey = "renamed";
+    updateProjectKey(PROJECT_KEY, newProjectKey, false);
 
-    assertThat(tester.wsClient().components().show(new ShowRequest().setComponentId(project.getId())).getComponent().getKey()).isEqualTo(newProjectKey);
+    assertThat(tester.projects().exists(PROJECT_KEY)).isFalse();
+    assertThat(tester.projects().exists(newProjectKey)).isTrue();
   }
 
   @Test
@@ -85,7 +99,7 @@ public class ProjectKeyUpdateTest {
     Organizations.Organization organization = tester.organizations().generate();
     Projects.CreateWsResponse.Project project = createProject(organization, "one", "Foo");
 
-    updateKey(project, "two");
+    updateProjectKey(project.getKey(), "two", false);
 
     assertThat(isProjectInDatabase("one")).isFalse();
     assertThat(isProjectInDatabase("two")).isTrue();
@@ -102,7 +116,7 @@ public class ProjectKeyUpdateTest {
 
     lockWritesOnProjectIndices();
 
-    updateKey(project, "two");
+    updateProjectKey(project.getKey(), "two", false);
 
     assertThat(isProjectInDatabase("one")).isFalse();
 
@@ -142,7 +156,7 @@ public class ProjectKeyUpdateTest {
     String initialKey = "com.sonarsource.it.samples:multi-modules-sample:module_a";
     String newKey = "com.sonarsource.it.samples:multi-modules-sample:module_c";
 
-    updateKey(initialKey, newKey);
+    updateModuleKey(initialKey, newKey);
 
     assertThat(isComponentInDatabase(initialKey)).isFalse();
     assertThat(isComponentInDatabase(newKey)).isTrue();
@@ -168,7 +182,7 @@ public class ProjectKeyUpdateTest {
     String newKey = "com.sonarsource.it.samples:multi-modules-sample:module_c";
 
     lockWritesOnProjectIndices();
-    updateKey(initialKey, newKey);
+    updateModuleKey(initialKey, newKey);
 
     // api/components/search loads keys from db, so results are consistent
     assertThat(isComponentInDatabase(initialKey)).isFalse();
@@ -193,7 +207,133 @@ public class ProjectKeyUpdateTest {
       Thread.sleep(1_000L);
       recovered = keysInComponentSuggestions(newKey).contains(newKey) && keysInComponentSuggestions(initialKey).isEmpty();
     }
+  }
+
+  /**
+   * SONAR-10511
+   */
+  @Test
+  public void update_key_of_disabled_files() throws Exception {
+    Organizations.Organization organization = tester.organizations().generate();
 
+    // first analysis
+    File projectWith2Files = new XooProjectBuilder(PROJECT_KEY)
+      .setFilesPerModule(2)
+      .build(temp.newFolder());
+    analyze(organization, projectWith2Files);
+    assertThat(countFilesInProject()).isEqualTo(2);
+
+    // second analysis emulates a deletion of file
+    File projectWith1File = new XooProjectBuilder(PROJECT_KEY)
+      .setFilesPerModule(1)
+      .build(temp.newFolder());
+    analyze(organization, projectWith1File);
+    assertThat(countFilesInProject()).isEqualTo(1);
+
+    // update the project key
+    updateProjectKey(PROJECT_KEY, "renamed", false);
+    ItUtils.expectNotFoundError(() -> tester.wsClient().components().show(new ShowRequest().setComponent(PROJECT_KEY)));
+
+    // first analysis of the new project, which re-enables the deleted file
+    analyze(organization, projectWith2Files);
+    assertThat(countFilesInProject()).isEqualTo(2);
+  }
+
+  /**
+   * SONAR-10511
+   */
+  @Test
+  public void update_of_project_key_includes_disabled_modules() throws Exception {
+    Organizations.Organization organization = tester.organizations().generate();
+
+    // first analysis
+    File projectWithModulesAB = new XooProjectBuilder(PROJECT_KEY)
+      .addModules("module_a", "module_b")
+      .build(temp.newFolder());
+    analyze(organization, projectWithModulesAB);
+    assertThat(countFilesInProject()).isEqualTo(3);
+
+    // second analysis emulates deletion of module_b
+    File projectWithModuleA = new XooProjectBuilder(PROJECT_KEY)
+      .addModules("module_a")
+      .build(temp.newFolder());
+    analyze(organization, projectWithModuleA);
+    assertThat(countFilesInProject()).isEqualTo(2);
+
+    // update the project key
+    updateProjectKey(PROJECT_KEY, "renamed", false);
+    assertThat(tester.projects().exists(PROJECT_KEY)).isFalse();
+
+    // analysis of new project, re-enabling the deleted module
+    File projectWithModulesBC = new XooProjectBuilder(PROJECT_KEY)
+      .addModules("module_b", "module_c")
+      .build(temp.newFolder());
+    analyze(organization, projectWithModulesBC);
+    assertThat(countFilesInProject()).isEqualTo(3);
+  }
+
+  @Test
+  public void simulate_update_key_of_modules() throws Exception {
+    Organizations.Organization organization = tester.organizations().generate();
+
+    File project = new XooProjectBuilder(PROJECT_KEY)
+      .addModules("module_a", "module_b")
+      .build(temp.newFolder());
+    analyze(organization, project);
+    assertThat(tester.projects().exists(PROJECT_KEY)).isTrue();
+
+    // simulate update of project key
+    Projects.BulkUpdateKeyWsResponse response = updateProjectKey(PROJECT_KEY, "renamed", true);
+
+    assertThat(tester.projects().exists(PROJECT_KEY)).isTrue();
+    assertThat(tester.projects().exists("renamed")).isFalse();
+    assertThat(response.getKeysList())
+      .extracting(Key::getKey, Key::getNewKey)
+      .containsExactlyInAnyOrder(
+        tuple(PROJECT_KEY, "renamed"),
+        tuple(PROJECT_KEY + ":module_a", "renamed:module_a"),
+        tuple(PROJECT_KEY + ":module_b", "renamed:module_b"));
+  }
+
+  @Test
+  public void simulate_update_key_of_disabled_modules() throws Exception {
+    Organizations.Organization organization = tester.organizations().generate();
+
+    // first analysis
+    File projectWithModulesAB = new XooProjectBuilder(PROJECT_KEY)
+      .addModules("module_a", "module_b")
+      .build(temp.newFolder());
+    analyze(organization, projectWithModulesAB);
+    assertThat(countFilesInProject()).isEqualTo(3);
+
+    // second analysis emulates deletion of module_b
+    File projectWithModuleA = new XooProjectBuilder(PROJECT_KEY)
+      .addModules("module_a")
+      .build(temp.newFolder());
+    analyze(organization, projectWithModuleA);
+    assertThat(countFilesInProject()).isEqualTo(2);
+
+    // update the project key
+    Projects.BulkUpdateKeyWsResponse response = updateProjectKey(PROJECT_KEY, "renamed", true);
+
+    assertThat(tester.projects().exists(PROJECT_KEY)).isTrue();
+    assertThat(tester.projects().exists("renamed")).isFalse();
+    assertThat(response.getKeysList())
+      .extracting(Key::getKey, Key::getNewKey)
+      .containsExactlyInAnyOrder(
+        tuple(PROJECT_KEY, "renamed"),
+        tuple(PROJECT_KEY + ":module_a", "renamed:module_a"));
+  }
+
+  private int countFilesInProject() {
+    TreeRequest request = new TreeRequest().setComponent(PROJECT_KEY).setQualifiers(asList("FIL"));
+    return tester.wsClient().components().tree(request).getComponentsCount();
+  }
+
+  private void analyze(Organizations.Organization organization, File projectDir) {
+    orchestrator.executeBuild(SonarScanner.create(projectDir,
+      "sonar.organization", organization.getKey(),
+      "sonar.login", "admin", "sonar.password", "admin"));
   }
 
   private void lockWritesOnProjectIndices() throws Exception {
@@ -206,17 +346,20 @@ public class ProjectKeyUpdateTest {
     tester.elasticsearch().unlockWrites("projectmeasures");
   }
 
-  private void updateKey(Projects.CreateWsResponse.Project project, String newKey) {
-    tester.wsClient().projects().updateKey(new UpdateKeyRequest().setFrom(project.getKey()).setTo(newKey));
+  private void updateModuleKey(String initialKey, String newKey) {
+    tester.wsClient().projects().updateKey(new UpdateKeyRequest().setFrom(initialKey).setTo(newKey));
   }
 
-  private void updateKey(String initialKey, String newKey) {
-    tester.wsClient().projects().updateKey(new UpdateKeyRequest().setFrom(initialKey).setTo(newKey));
+  private Projects.BulkUpdateKeyWsResponse updateProjectKey(String initialKey, String newKey, boolean dryRun) {
+    return tester.wsClient().projects().bulkUpdateKey(new BulkUpdateKeyRequest()
+      .setProject(initialKey)
+      .setFrom(initialKey)
+      .setTo(newKey)
+      .setDryRun(String.valueOf(dryRun)));
   }
 
   private Projects.CreateWsResponse.Project createProject(Organizations.Organization organization, String key, String name) {
-    CreateRequest createRequest = new CreateRequest().setProject(key).setName(name).setOrganization(organization.getKey());
-    return tester.wsClient().projects().create(createRequest).getProject();
+    return tester.projects().provision(organization, r -> r.setProject(key).setName(name));
   }
 
   private boolean isProjectInDatabase(String projectKey) {
@@ -255,8 +398,58 @@ public class ProjectKeyUpdateTest {
       .collect(Collectors.toList());
   }
 
-  private void analyzeXooSample() {
-    SonarScanner build = SonarScanner.create(projectDir("shared/xoo-sample"));
-    orchestrator.executeBuild(build);
+  private static class XooProjectBuilder {
+    private final String key;
+    private final List<String> moduleKeys = new ArrayList<>();
+    private int filesPerModule = 1;
+
+    XooProjectBuilder(String projectKey) {
+      this.key = projectKey;
+    }
+
+    XooProjectBuilder addModules(String key, String... otherKeys) {
+      this.moduleKeys.add(key);
+      this.moduleKeys.addAll(asList(otherKeys));
+      return this;
+    }
+
+    XooProjectBuilder setFilesPerModule(int i) {
+      this.filesPerModule = i;
+      return this;
+    }
+
+    File build(File dir) {
+      for (String moduleKey : moduleKeys) {
+        generateModule(moduleKey, new File(dir, moduleKey), new Properties());
+      }
+      Properties additionalProps = new Properties();
+      additionalProps.setProperty("sonar.modules", StringUtils.join(moduleKeys, ","));
+      generateModule(key, dir, additionalProps);
+      return dir;
+    }
+
+    private void generateModule(String key, File dir, Properties additionalProps) {
+      try {
+        File sourceDir = new File(dir, "src");
+        FileUtils.forceMkdir(sourceDir);
+        for (int i = 0; i < filesPerModule; i++) {
+          File sourceFile = new File(sourceDir, "File" + i + ".xoo");
+          FileUtils.write(sourceFile, "content of " + sourceFile.getName());
+        }
+        Properties props = new Properties();
+        props.setProperty("sonar.projectKey", key);
+        props.setProperty("sonar.projectName", key);
+        props.setProperty("sonar.projectVersion", "1.0");
+        props.setProperty("sonar.sources", sourceDir.getName());
+        props.putAll(additionalProps);
+        File propsFile = new File(dir, "sonar-project.properties");
+        try (OutputStream output = FileUtils.openOutputStream(propsFile)) {
+          props.store(output, "generated");
+        }
+      } catch (IOException e) {
+        throw new IllegalStateException(e);
+      }
+    }
   }
+
 }