]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14742 Project import from GitHub, Bitbucket and Azure can clash with existing...
authorAurelien Poscia <aurelien.poscia@sonarsource.com>
Mon, 14 Mar 2022 13:28:20 +0000 (14:28 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 17 Mar 2022 20:03:08 +0000 (20:03 +0000)
16 files changed:
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/ProjectKeyGenerator.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/bitbucketserver/ImportBitbucketServerProjectAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/ImportHelperTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/ProjectKeyGeneratorTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketcloud/ImportBitbucketCloudRepoActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/bitbucketserver/ImportBitbucketServerProjectActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/github/ImportGithubProjectActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/gitlab/ImportGitLabProjectActionTest.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
sonar-core/src/main/java/org/sonar/core/component/ComponentKeys.java
sonar-core/src/test/java/org/sonar/core/component/ComponentKeysSanitizationTest.java [new file with mode: 0644]

diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/ProjectKeyGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/ProjectKeyGenerator.java
new file mode 100644 (file)
index 0000000..7f2459e
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.almintegration.ws;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.List;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.core.util.UuidFactory;
+
+import static com.google.common.collect.Lists.asList;
+import static org.sonar.core.component.ComponentKeys.sanitizeProjectKey;
+
+public class ProjectKeyGenerator {
+
+  @VisibleForTesting
+  static final int MAX_PROJECT_KEY_SIZE = 250;
+  @VisibleForTesting
+  static final Character PROJECT_KEY_SEPARATOR = '_';
+
+  private final UuidFactory uuidFactory;
+
+  public ProjectKeyGenerator(UuidFactory uuidFactory) {
+    this.uuidFactory = uuidFactory;
+  }
+
+  public String generateUniqueProjectKey(String projectName, String... extraProjectKeyItems) {
+    String sqProjectKey = generateCompleteProjectKey(projectName, extraProjectKeyItems);
+    sqProjectKey = truncateProjectKeyIfNecessary(sqProjectKey);
+    return sanitizeProjectKey(sqProjectKey);
+  }
+
+  private String generateCompleteProjectKey(String projectName, String[] extraProjectKeyItems) {
+    List<String> projectKeyItems = asList(projectName, extraProjectKeyItems);
+    String projectKey = StringUtils.join(projectKeyItems, PROJECT_KEY_SEPARATOR);
+    String uuid = uuidFactory.create();
+    return projectKey + PROJECT_KEY_SEPARATOR + uuid;
+  }
+
+  private static String truncateProjectKeyIfNecessary(String sqProjectKey) {
+    if (sqProjectKey.length() > MAX_PROJECT_KEY_SIZE) {
+      return sqProjectKey.substring(sqProjectKey.length() - MAX_PROJECT_KEY_SIZE);
+    }
+    return sqProjectKey;
+  }
+
+}
index 9441e4a839799757b86f70b6838fa78cacc05875..c44e393a92b2d1f32758ee464d1bd61bbcb9470f 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.almintegration.ws.azure;
 
-import com.google.common.annotations.VisibleForTesting;
 import java.util.Optional;
 import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
 import org.sonar.alm.client.azure.GsonAzureRepo;
@@ -34,6 +33,7 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.project.ProjectDefaultVisibility;
 import org.sonar.server.user.UserSession;
@@ -57,16 +57,18 @@ public class ImportAzureProjectAction implements AlmIntegrationsWsAction {
   private final ProjectDefaultVisibility projectDefaultVisibility;
   private final ComponentUpdater componentUpdater;
   private final ImportHelper importHelper;
+  private final ProjectKeyGenerator projectKeyGenerator;
 
   public ImportAzureProjectAction(DbClient dbClient, UserSession userSession, AzureDevOpsHttpClient azureDevOpsHttpClient,
     ProjectDefaultVisibility projectDefaultVisibility, ComponentUpdater componentUpdater,
-    ImportHelper importHelper) {
+    ImportHelper importHelper, ProjectKeyGenerator projectKeyGenerator) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.azureDevOpsHttpClient = azureDevOpsHttpClient;
     this.projectDefaultVisibility = projectDefaultVisibility;
     this.componentUpdater = componentUpdater;
     this.importHelper = importHelper;
+    this.projectKeyGenerator = projectKeyGenerator;
   }
 
   @Override
@@ -128,8 +130,9 @@ public class ImportAzureProjectAction implements AlmIntegrationsWsAction {
 
   private ComponentDto createProject(DbSession dbSession, GsonAzureRepo repo) {
     boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate();
+    String uniqueProjectKey = projectKeyGenerator.generateUniqueProjectKey(repo.getProject().getName(), repo.getName());
     return componentUpdater.createWithoutCommit(dbSession, newComponentBuilder()
-        .setKey(generateProjectKey(repo.getProject().getName(), repo.getName()))
+        .setKey(uniqueProjectKey)
         .setName(repo.getName())
         .setPrivate(visibility)
         .setQualifier(PROJECT)
@@ -152,15 +155,4 @@ public class ImportAzureProjectAction implements AlmIntegrationsWsAction {
       componentDto.name(), componentDto.getKey());
   }
 
-  @VisibleForTesting
-  String generateProjectKey(String projectName, String repoName) {
-    String sqProjectKey = projectName + "_" + repoName;
-
-    if (sqProjectKey.length() > 250) {
-      sqProjectKey = sqProjectKey.substring(sqProjectKey.length() - 250);
-    }
-
-    return sqProjectKey.replace(" ", "_");
-  }
-
 }
index 92eaf03522b54dae8ad32cb3f064ee64a7c41f54..8a2fc032ab0f0178bbb086298091e4170d1c944f 100644 (file)
@@ -33,6 +33,7 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.component.NewComponent;
 import org.sonar.server.project.ProjectDefaultVisibility;
@@ -56,15 +57,18 @@ public class ImportBitbucketCloudRepoAction implements AlmIntegrationsWsAction {
   private final ProjectDefaultVisibility projectDefaultVisibility;
   private final ComponentUpdater componentUpdater;
   private final ImportHelper importHelper;
+  private final ProjectKeyGenerator projectKeyGenerator;
 
   public ImportBitbucketCloudRepoAction(DbClient dbClient, UserSession userSession, BitbucketCloudRestClient bitbucketCloudRestClient,
-    ProjectDefaultVisibility projectDefaultVisibility, ComponentUpdater componentUpdater, ImportHelper importHelper) {
+    ProjectDefaultVisibility projectDefaultVisibility, ComponentUpdater componentUpdater, ImportHelper importHelper,
+    ProjectKeyGenerator projectKeyGenerator) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.bitbucketCloudRestClient = bitbucketCloudRestClient;
     this.projectDefaultVisibility = projectDefaultVisibility;
     this.componentUpdater = componentUpdater;
     this.importHelper = importHelper;
+    this.projectKeyGenerator = projectKeyGenerator;
   }
 
   @Override
@@ -124,8 +128,9 @@ public class ImportBitbucketCloudRepoAction implements AlmIntegrationsWsAction {
 
   private ComponentDto createProject(DbSession dbSession, String workspace, Repository repo, @Nullable String defaultBranchName) {
     boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate();
+    String uniqueProjectKey = projectKeyGenerator.generateUniqueProjectKey(workspace, repo.getSlug());
     NewComponent newProject = newComponentBuilder()
-      .setKey(workspace + "_" + repo.getSlug())
+      .setKey(uniqueProjectKey)
       .setName(repo.getName())
       .setPrivate(visibility)
       .setQualifier(PROJECT)
index 7c2cd6be9d620d69d6c60ea44043c8a1beaccf8d..0459fdc797c69f0b1038d57cfb56e43fe83cdb5e 100644 (file)
@@ -36,6 +36,7 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.component.NewComponent;
 import org.sonar.server.project.ProjectDefaultVisibility;
@@ -60,16 +61,18 @@ public class ImportBitbucketServerProjectAction implements AlmIntegrationsWsActi
   private final ProjectDefaultVisibility projectDefaultVisibility;
   private final ComponentUpdater componentUpdater;
   private final ImportHelper importHelper;
+  private final ProjectKeyGenerator projectKeyGenerator;
 
   public ImportBitbucketServerProjectAction(DbClient dbClient, UserSession userSession, BitbucketServerRestClient bitbucketServerRestClient,
     ProjectDefaultVisibility projectDefaultVisibility, ComponentUpdater componentUpdater,
-    ImportHelper importHelper) {
+    ImportHelper importHelper, ProjectKeyGenerator projectKeyGenerator) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.bitbucketServerRestClient = bitbucketServerRestClient;
     this.projectDefaultVisibility = projectDefaultVisibility;
     this.componentUpdater = componentUpdater;
     this.importHelper = importHelper;
+    this.projectKeyGenerator = projectKeyGenerator;
   }
 
   @Override
@@ -141,8 +144,9 @@ public class ImportBitbucketServerProjectAction implements AlmIntegrationsWsActi
 
   private ComponentDto createProject(DbSession dbSession, Repository repo, @Nullable String defaultBranchName) {
     boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate();
+    String uniqueProjectKey = projectKeyGenerator.generateUniqueProjectKey(repo.getProject().getKey(), repo.getSlug());
     NewComponent newProject = newComponentBuilder()
-      .setKey(getProjectKey(repo))
+      .setKey(uniqueProjectKey)
       .setName(repo.getName())
       .setPrivate(visibility)
       .setQualifier(PROJECT)
@@ -165,9 +169,4 @@ public class ImportBitbucketServerProjectAction implements AlmIntegrationsWsActi
       componentDto.name(), componentDto.getKey());
   }
 
-  private static String getProjectKey(Repository repo) {
-    String key = repo.getProject().getKey() + "_" + repo.getSlug();
-    return key.replace("~", "");
-  }
-
 }
index 796a4dcd2df384c997a710be86e043ef15ec9aad..453d4e98d7dd639cd6f03602e468852280909107 100644 (file)
@@ -35,6 +35,7 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.project.ProjectDefaultVisibility;
@@ -59,15 +60,17 @@ public class ImportGithubProjectAction implements AlmIntegrationsWsAction {
   private final GithubApplicationClient githubApplicationClient;
   private final ComponentUpdater componentUpdater;
   private final ImportHelper importHelper;
+  private final ProjectKeyGenerator projectKeyGenerator;
 
   public ImportGithubProjectAction(DbClient dbClient, UserSession userSession, ProjectDefaultVisibility projectDefaultVisibility,
-    GithubApplicationClientImpl githubApplicationClient, ComponentUpdater componentUpdater, ImportHelper importHelper) {
+    GithubApplicationClientImpl githubApplicationClient, ComponentUpdater componentUpdater, ImportHelper importHelper, ProjectKeyGenerator projectKeyGenerator) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.projectDefaultVisibility = projectDefaultVisibility;
     this.githubApplicationClient = githubApplicationClient;
     this.componentUpdater = componentUpdater;
     this.importHelper = importHelper;
+    this.projectKeyGenerator = projectKeyGenerator;
   }
 
   @Override
@@ -131,19 +134,16 @@ public class ImportGithubProjectAction implements AlmIntegrationsWsAction {
 
   private ComponentDto createProject(DbSession dbSession, Repository repo, String mainBranchName) {
     boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate();
+    String uniqueProjectKey = projectKeyGenerator.generateUniqueProjectKey(repo.getFullName());
     return componentUpdater.createWithoutCommit(dbSession, newComponentBuilder()
-      .setKey(getProjectKeyFromRepository(repo))
-      .setName(repo.getName())
-      .setPrivate(visibility)
-      .setQualifier(PROJECT)
-      .build(),
+        .setKey(uniqueProjectKey)
+        .setName(repo.getName())
+        .setPrivate(visibility)
+        .setQualifier(PROJECT)
+        .build(),
       userSession.getUuid(), userSession.getLogin(), mainBranchName, s -> {});
   }
 
-  static String getProjectKeyFromRepository(Repository repo) {
-    return repo.getFullName().replace("/", "_");
-  }
-
   private void populatePRSetting(DbSession dbSession, Repository repo, ComponentDto componentDto, AlmSettingDto almSettingDto) {
     ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto()
       .setAlmSettingUuid(almSettingDto.getUuid())
index 86b366ea039ee28b5292603ec69c94271a10379c..138ecb89723a79ab40997ce82ed135f2b9b70a4b 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.almintegration.ws.gitlab;
 
-import com.google.common.annotations.VisibleForTesting;
 import java.util.Optional;
 import javax.annotation.Nullable;
 import org.sonar.alm.client.gitlab.GitLabBranch;
@@ -28,7 +27,6 @@ import org.sonar.alm.client.gitlab.Project;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.core.util.UuidFactory;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.alm.pat.AlmPatDto;
@@ -37,6 +35,7 @@ import org.sonar.db.alm.setting.ProjectAlmSettingDto;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.project.ProjectDefaultVisibility;
 import org.sonar.server.user.UserSession;
@@ -56,19 +55,19 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {
   private final ProjectDefaultVisibility projectDefaultVisibility;
   private final GitlabHttpClient gitlabHttpClient;
   private final ComponentUpdater componentUpdater;
-  private final UuidFactory uuidFactory;
   private final ImportHelper importHelper;
+  private final ProjectKeyGenerator projectKeyGenerator;
 
   public ImportGitLabProjectAction(DbClient dbClient, UserSession userSession,
     ProjectDefaultVisibility projectDefaultVisibility, GitlabHttpClient gitlabHttpClient,
-    ComponentUpdater componentUpdater, UuidFactory uuidFactory, ImportHelper importHelper) {
+    ComponentUpdater componentUpdater, ImportHelper importHelper, ProjectKeyGenerator projectKeyGenerator) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.projectDefaultVisibility = projectDefaultVisibility;
     this.gitlabHttpClient = gitlabHttpClient;
     this.componentUpdater = componentUpdater;
-    this.uuidFactory = uuidFactory;
     this.importHelper = importHelper;
+    this.projectKeyGenerator = projectKeyGenerator;
   }
 
   @Override
@@ -135,10 +134,10 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {
 
   private ComponentDto createProject(DbSession dbSession, Project gitlabProject, @Nullable String mainBranchName) {
     boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate();
-    String sqProjectKey = generateProjectKey(gitlabProject.getPathWithNamespace(), uuidFactory.create());
+    String uniqueProjectKey = projectKeyGenerator.generateUniqueProjectKey(gitlabProject.getPathWithNamespace());
 
     return componentUpdater.createWithoutCommit(dbSession, newComponentBuilder()
-        .setKey(sqProjectKey)
+        .setKey(uniqueProjectKey)
         .setName(gitlabProject.getName())
         .setPrivate(visibility)
         .setQualifier(PROJECT)
@@ -147,14 +146,4 @@ public class ImportGitLabProjectAction implements AlmIntegrationsWsAction {
       });
   }
 
-  @VisibleForTesting
-  String generateProjectKey(String pathWithNamespace, String uuid) {
-    String sqProjectKey = pathWithNamespace + "_" + uuid;
-
-    if (sqProjectKey.length() > 250) {
-      sqProjectKey = sqProjectKey.substring(sqProjectKey.length() - 250);
-    }
-
-    return sqProjectKey.replace("/", "_");
-  }
 }
index 3ea30f971dfb75f585724a5d1e93d9e0efd48f7f..828be41dc10b1a9674f16bd4b4347ab4d59fc4de 100644 (file)
@@ -24,8 +24,6 @@ import org.junit.Test;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
-import org.sonar.db.component.BranchDto;
-import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
 import org.sonar.server.exceptions.NotFoundException;
@@ -42,11 +40,6 @@ public class ImportHelperTest {
 
   private final System2 system2 = System2.INSTANCE;
   private final ComponentDto componentDto = ComponentTesting.newPublicProjectDto();
-  private final BranchDto branchDto = new BranchDto()
-    .setBranchType(BranchType.BRANCH)
-    .setKey("main")
-    .setUuid(componentDto.uuid())
-    .setProjectUuid(componentDto.uuid());
   private final Request request = mock(Request.class);
 
   @Rule
@@ -55,7 +48,7 @@ public class ImportHelperTest {
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
-  private ImportHelper underTest = new ImportHelper(db.getDbClient(), userSession);
+  private final ImportHelper underTest = new ImportHelper(db.getDbClient(), userSession);
 
   @Test
   public void it_throws_exception_when_provisioning_project_without_permission() {
@@ -91,7 +84,7 @@ public class ImportHelperTest {
     CreateWsResponse.Project project = response.getProject();
 
     assertThat(project).extracting(CreateWsResponse.Project::getKey, CreateWsResponse.Project::getName,
-      CreateWsResponse.Project::getQualifier)
+        CreateWsResponse.Project::getQualifier)
       .containsExactly(componentDto.getDbKey(), componentDto.name(), componentDto.qualifier());
   }
 }
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/ProjectKeyGeneratorTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almintegration/ws/ProjectKeyGeneratorTest.java
new file mode 100644 (file)
index 0000000..c8e6c62
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.almintegration.ws;
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.core.util.UuidFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.almintegration.ws.ProjectKeyGenerator.MAX_PROJECT_KEY_SIZE;
+import static org.sonar.server.almintegration.ws.ProjectKeyGenerator.PROJECT_KEY_SEPARATOR;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ProjectKeyGeneratorTest {
+
+  private static final int MAX_UUID_SIZE = 40;
+  private static final String UUID_STRING = RandomStringUtils.randomAlphanumeric(MAX_UUID_SIZE);
+
+  @Mock
+  private UuidFactory uuidFactory;
+
+  @InjectMocks
+  private ProjectKeyGenerator projectKeyGenerator;
+
+  @Before
+  public void setUp() {
+    when(uuidFactory.create()).thenReturn(UUID_STRING);
+  }
+
+  @Test
+  public void generateUniqueProjectKey_shortProjectName_shouldAppendUuid() {
+    String fullProjectName = RandomStringUtils.randomAlphanumeric(10);
+
+    assertThat(projectKeyGenerator.generateUniqueProjectKey(fullProjectName))
+      .isEqualTo(generateExpectedKeyName(fullProjectName));
+  }
+
+  @Test
+  public void generateUniqueProjectKey_projectNameEqualsToMaximumSize_shouldTruncateProjectNameAndPreserveUUID() {
+    String fullProjectName = RandomStringUtils.randomAlphanumeric(MAX_PROJECT_KEY_SIZE);
+
+    String projectKey = projectKeyGenerator.generateUniqueProjectKey(fullProjectName);
+    assertThat(projectKey)
+      .hasSize(MAX_PROJECT_KEY_SIZE)
+      .isEqualTo(generateExpectedKeyName(fullProjectName.substring(fullProjectName.length() + UUID_STRING.length() + 1 - MAX_PROJECT_KEY_SIZE)));
+  }
+
+  @Test
+  public void generateUniqueProjectKey_projectNameBiggerThanMaximumSize_shouldTruncateProjectNameAndPreserveUUID() {
+    String fullProjectName = RandomStringUtils.randomAlphanumeric(MAX_PROJECT_KEY_SIZE + 50);
+
+    String projectKey = projectKeyGenerator.generateUniqueProjectKey(fullProjectName);
+    assertThat(projectKey)
+      .hasSize(MAX_PROJECT_KEY_SIZE)
+      .isEqualTo(generateExpectedKeyName(fullProjectName.substring(fullProjectName.length() + UUID_STRING.length() + 1 - MAX_PROJECT_KEY_SIZE)));
+  }
+
+  @Test
+  public void generateUniqueProjectKey_projectNameContainsSlashes_shouldBeEscaped() {
+    String fullProjectName = "a/b/c";
+
+    assertThat(projectKeyGenerator.generateUniqueProjectKey(fullProjectName))
+      .isEqualTo(generateExpectedKeyName(fullProjectName.replace("/", "_")));
+  }
+
+  private String generateExpectedKeyName(String truncatedProjectName) {
+    return truncatedProjectName + PROJECT_KEY_SEPARATOR + UUID_STRING;
+  }
+}
index 424c9e00e6fa8214a96f2a1307b319bdcdcdff33..6b5d25f5ca82da5eb39bc479fa43dc406aed03f2 100644 (file)
@@ -20,8 +20,6 @@
 package org.sonar.server.almintegration.ws.azure;
 
 import java.util.Optional;
-import java.util.stream.IntStream;
-
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -41,6 +39,7 @@ import org.sonar.db.component.BranchDto;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
@@ -56,12 +55,12 @@ import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
 import org.sonarqube.ws.Projects;
 
-import static java.util.stream.Collectors.joining;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.tuple;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.sonar.db.alm.integration.pat.AlmPatsTesting.newAlmPatDto;
 import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
@@ -69,6 +68,8 @@ import static org.sonar.db.permission.GlobalPermission.SCAN;
 
 public class ImportAzureProjectActionTest {
 
+  private static final String GENERATED_PROJECT_KEY = "TEST_PROJECT_KEY";
+
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
   @Rule
@@ -82,13 +83,15 @@ public class ImportAzureProjectActionTest {
   private final Encryption encryption = mock(Encryption.class);
   private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession);
   private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class);
+  private final ProjectKeyGenerator projectKeyGenerator = mock(ProjectKeyGenerator.class);
   private final ImportAzureProjectAction importAzureProjectAction = new ImportAzureProjectAction(db.getDbClient(), userSession,
-    azureDevOpsHttpClient, projectDefaultVisibility, componentUpdater, importHelper);
+    azureDevOpsHttpClient, projectDefaultVisibility, componentUpdater, importHelper, projectKeyGenerator);
   private final WsActionTester ws = new WsActionTester(importAzureProjectAction);
 
   @Before
   public void before() {
     when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE);
+    when(projectKeyGenerator.generateUniqueProjectKey(any(), any())).thenReturn(GENERATED_PROJECT_KEY);
   }
 
   @Test
@@ -113,7 +116,7 @@ public class ImportAzureProjectActionTest {
       .executeProtobuf(Projects.CreateWsResponse.class);
 
     Projects.CreateWsResponse.Project result = response.getProject();
-    assertThat(result.getKey()).isEqualTo(repo.getProject().getName() + "_" + repo.getName());
+    assertThat(result.getKey()).isEqualTo(GENERATED_PROJECT_KEY);
     assertThat(result.getName()).isEqualTo(repo.getName());
 
     Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
@@ -132,6 +135,8 @@ public class ImportAzureProjectActionTest {
       .findFirst();
     assertThat(mainBranch).isPresent();
     assertThat(mainBranch.get().getKey()).hasToString("repo-default-branch");
+
+    verify(projectKeyGenerator).generateUniqueProjectKey(repo.getProject().getName(), repo.getName());
   }
 
   @Test
@@ -236,8 +241,7 @@ public class ImportAzureProjectActionTest {
       dto.setUserUuid(user.getUuid());
     });
     GsonAzureRepo repo = getGsonAzureRepo();
-    String projectKey = repo.getProject().getName() + "_" + repo.getName();
-    db.components().insertPublicProject(p -> p.setDbKey(projectKey));
+    db.components().insertPublicProject(p -> p.setDbKey(GENERATED_PROJECT_KEY));
 
     when(azureDevOpsHttpClient.getRepo(almSetting.getUrl(), almSetting.getDecryptedPersonalAccessToken(encryption),
       "project-name", "repo-name")).thenReturn(repo);
@@ -248,21 +252,7 @@ public class ImportAzureProjectActionTest {
 
     assertThatThrownBy(request::execute)
       .isInstanceOf(BadRequestException.class)
-      .hasMessage("Could not create null, key already exists: " + projectKey);
-  }
-
-  @Test
-  public void sanitize_project_and_repo_names_with_invalid_characters() {
-    assertThat(importAzureProjectAction.generateProjectKey("project name", "repo name"))
-      .isEqualTo("project_name_repo_name");
-  }
-
-  @Test
-  public void sanitize_long_project_and_repo_names() {
-    String projectName = IntStream.range(0, 260).mapToObj(i -> "a").collect(joining());
-
-    assertThat(importAzureProjectAction.generateProjectKey(projectName, "repo name"))
-      .hasSize(250);
+      .hasMessage("Could not create null, key already exists: " + GENERATED_PROJECT_KEY);
   }
 
   @Test
index da4c6cfbe7ffc806f5e3d1943de865f62493a29f..6e50410f731aff5346ab78a2a59e608659f6e2ee 100644 (file)
@@ -39,6 +39,7 @@ import org.sonar.db.component.BranchDto;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
@@ -54,17 +55,22 @@ import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
 import org.sonarqube.ws.Projects;
 
+import static java.util.Objects.requireNonNull;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.tuple;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.sonar.db.alm.integration.pat.AlmPatsTesting.newAlmPatDto;
 import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
 import static org.sonar.db.permission.GlobalPermission.SCAN;
 
 public class ImportBitbucketCloudRepoActionTest {
+
+  private static final String GENERATED_PROJECT_KEY = "TEST_PROJECT_KEY";
+
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
   @Rule
@@ -77,12 +83,14 @@ public class ImportBitbucketCloudRepoActionTest {
     mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()), new TestProjectIndexers(), new SequenceUuidFactory());
 
   private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession);
+  private final ProjectKeyGenerator projectKeyGenerator = mock(ProjectKeyGenerator.class);
   private final WsActionTester ws = new WsActionTester(new ImportBitbucketCloudRepoAction(db.getDbClient(), userSession,
-    bitbucketCloudRestClient, projectDefaultVisibility, componentUpdater, importHelper));
+    bitbucketCloudRestClient, projectDefaultVisibility, componentUpdater, importHelper, projectKeyGenerator));
 
   @Before
   public void before() {
     when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE);
+    when(projectKeyGenerator.generateUniqueProjectKey(any(), any())).thenReturn(GENERATED_PROJECT_KEY);
   }
 
   @Test
@@ -103,7 +111,7 @@ public class ImportBitbucketCloudRepoActionTest {
       .executeProtobuf(Projects.CreateWsResponse.class);
 
     Projects.CreateWsResponse.Project result = response.getProject();
-    assertThat(result.getKey()).isEqualTo(almSetting.getAppId() + "_" + repo.getSlug());
+    assertThat(result.getKey()).isEqualTo(GENERATED_PROJECT_KEY);
     assertThat(result.getName()).isEqualTo(repo.getName());
 
     Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
@@ -115,6 +123,7 @@ public class ImportBitbucketCloudRepoActionTest {
     Optional<BranchDto> branchDto = db.getDbClient().branchDao().selectByBranchKey(db.getSession(), projectDto.get().getUuid(), "develop");
     assertThat(branchDto).isPresent();
     assertThat(branchDto.get().isMain()).isTrue();
+    verify(projectKeyGenerator).generateUniqueProjectKey(requireNonNull(almSetting.getAppId()), repo.getSlug());
   }
 
   @Test
@@ -127,8 +136,7 @@ public class ImportBitbucketCloudRepoActionTest {
       dto.setUserUuid(user.getUuid());
     });
     Repository repo = getGsonBBCRepo();
-    String projectKey = almSetting.getAppId() + "_" + repo.getSlug();
-    db.components().insertPublicProject(p -> p.setDbKey(projectKey));
+    db.components().insertPublicProject(p -> p.setDbKey(GENERATED_PROJECT_KEY));
 
     when(bitbucketCloudRestClient.getRepo(any(), any(), any())).thenReturn(repo);
 
@@ -138,7 +146,7 @@ public class ImportBitbucketCloudRepoActionTest {
 
     assertThatThrownBy(request::execute)
       .isInstanceOf(BadRequestException.class)
-      .hasMessageContaining("Could not create null, key already exists: " + projectKey);
+      .hasMessageContaining("Could not create null, key already exists: " + GENERATED_PROJECT_KEY);
   }
 
   @Test
index f1cc0ca6dde5513a62fe9ed60731e1b36b288dd4..ea7af30e4e882e3336951ee285dd0b9390c155f7 100644 (file)
@@ -44,6 +44,7 @@ import org.sonar.db.component.BranchDto;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.exceptions.BadRequestException;
@@ -58,6 +59,7 @@ import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsActionTester;
 import org.sonarqube.ws.Projects;
 
+import static java.util.Objects.requireNonNull;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.apache.commons.lang.math.JVMRandom.nextLong;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -65,12 +67,14 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.tuple;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.sonar.db.alm.integration.pat.AlmPatsTesting.newAlmPatDto;
 import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
 import static org.sonar.db.permission.GlobalPermission.SCAN;
 
 public class ImportBitbucketServerProjectActionTest {
+  private static final String GENERATED_PROJECT_KEY = "TEST_PROJECT_KEY";
 
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
@@ -84,8 +88,9 @@ public class ImportBitbucketServerProjectActionTest {
     mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()), new TestProjectIndexers(), new SequenceUuidFactory());
 
   private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession);
+  private final ProjectKeyGenerator projectKeyGenerator = mock(ProjectKeyGenerator.class);
   private final WsActionTester ws = new WsActionTester(new ImportBitbucketServerProjectAction(db.getDbClient(), userSession,
-    bitbucketServerRestClient, projectDefaultVisibility, componentUpdater, importHelper));
+    bitbucketServerRestClient, projectDefaultVisibility, componentUpdater, importHelper, projectKeyGenerator));
 
   private static BranchesList defaultBranchesList;
 
@@ -98,6 +103,7 @@ public class ImportBitbucketServerProjectActionTest {
   @Before
   public void before() {
     when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE);
+    when(projectKeyGenerator.generateUniqueProjectKey(any(), any())).thenReturn(GENERATED_PROJECT_KEY);
   }
 
   @Test
@@ -121,40 +127,13 @@ public class ImportBitbucketServerProjectActionTest {
       .executeProtobuf(Projects.CreateWsResponse.class);
 
     Projects.CreateWsResponse.Project result = response.getProject();
-    assertThat(result.getKey()).isEqualTo(project.getKey() + "_" + repo.getSlug());
+    assertThat(result.getKey()).isEqualTo(GENERATED_PROJECT_KEY);
     assertThat(result.getName()).isEqualTo(repo.getName());
 
     Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
     assertThat(projectDto).isPresent();
     assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get())).isPresent();
-  }
-
-  @Test
-  public void import_project_with_tilda() {
-    UserDto user = db.users().insertUser();
-    userSession.logIn(user).addPermission(PROVISION_PROJECTS);
-    AlmSettingDto almSetting = db.almSettings().insertGitHubAlmSetting();
-    db.almPats().insert(dto -> {
-      dto.setAlmSettingUuid(almSetting.getUuid());
-      dto.setUserUuid(user.getUuid());
-    });
-    Project project = getGsonBBSProject();
-    project.setKey("~" + project.getKey());
-    Repository repo = getGsonBBSRepo(project);
-    when(bitbucketServerRestClient.getRepo(any(), any(), any(), any())).thenReturn(repo);
-    when(bitbucketServerRestClient.getBranches(any(), any(), any(), any())).thenReturn(defaultBranchesList);
-
-    Projects.CreateWsResponse response = ws.newRequest()
-      .setParam("almSetting", almSetting.getKey())
-      .setParam("projectKey", "~projectKey")
-      .setParam("repositorySlug", "repo-slug")
-      .executeProtobuf(Projects.CreateWsResponse.class);
-
-    Projects.CreateWsResponse.Project result = response.getProject();
-
-    String key = project.getKey() + "_" + repo.getSlug();
-    assertThat(result.getKey()).isNotEqualTo(key);
-    assertThat(result.getKey()).isEqualTo(key.substring(1));
+    verify(projectKeyGenerator).generateUniqueProjectKey(requireNonNull(project.getKey()), repo.getSlug());
   }
 
   @Test
@@ -168,8 +147,7 @@ public class ImportBitbucketServerProjectActionTest {
     });
     Project project = getGsonBBSProject();
     Repository repo = getGsonBBSRepo(project);
-    String projectKey = project.getKey() + "_" + repo.getSlug();
-    db.components().insertPublicProject(p -> p.setDbKey(projectKey));
+    db.components().insertPublicProject(p -> p.setDbKey(GENERATED_PROJECT_KEY));
 
     assertThatThrownBy(() -> {
       when(bitbucketServerRestClient.getRepo(any(), any(), any(), any())).thenReturn(repo);
@@ -182,7 +160,7 @@ public class ImportBitbucketServerProjectActionTest {
         .execute();
     })
       .isInstanceOf(BadRequestException.class)
-      .hasMessage("Could not create null, key already exists: " + projectKey);
+      .hasMessage("Could not create null, key already exists: " + GENERATED_PROJECT_KEY);
   }
 
   @Test
index 3f95e76a037a1c77df278b371c72969cab630117..df54db59f9ca156051b19caaa0844d4450d9af6c 100644 (file)
@@ -36,9 +36,9 @@ import org.sonar.db.permission.GlobalPermission;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.es.TestProjectIndexers;
-import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.favorite.FavoriteUpdater;
@@ -63,6 +63,8 @@ import static org.sonar.server.tester.UserSessionRule.standalone;
 
 public class ImportGithubProjectActionTest {
 
+  private static final String PROJECT_KEY_NAME = "PROJECT_NAME";
+
   @Rule
   public UserSessionRule userSession = standalone();
 
@@ -76,9 +78,10 @@ public class ImportGithubProjectActionTest {
     mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()), new TestProjectIndexers(), new SequenceUuidFactory());
 
   private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession);
+  private final ProjectKeyGenerator projectKeyGenerator = mock(ProjectKeyGenerator.class);
   private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class);
   private final WsActionTester ws = new WsActionTester(new ImportGithubProjectAction(db.getDbClient(), userSession,
-    projectDefaultVisibility, appClient, componentUpdater, importHelper));
+    projectDefaultVisibility, appClient, componentUpdater, importHelper, projectKeyGenerator));
 
   @Before
   public void before() {
@@ -86,23 +89,23 @@ public class ImportGithubProjectActionTest {
   }
 
   @Test
-  public void import_project() {
+  public void importProject_ifProjectWithSameNameDoesNotExist_importSucceed() {
     AlmSettingDto githubAlmSetting = setupAlm();
     db.almPats().insert(p -> p.setAlmSettingUuid(githubAlmSetting.getUuid()).setUserUuid(userSession.getUuid()));
 
-    GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, "Hello-World", false, "octocat/Hello-World",
-      "https://github.sonarsource.com/api/v3/repos/octocat/Hello-World", "default-branch");
-    when(appClient.getRepository(any(), any(), any(), any()))
-      .thenReturn(Optional.of(repository));
+    GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, PROJECT_KEY_NAME, false, "octocat/" + PROJECT_KEY_NAME,
+      "https://github.sonarsource.com/api/v3/repos/octocat/" + PROJECT_KEY_NAME, "default-branch");
+    when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
+    when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
 
     Projects.CreateWsResponse response = ws.newRequest()
       .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
       .setParam(PARAM_ORGANIZATION, "octocat")
-      .setParam(PARAM_REPOSITORY_KEY, "octocat/Hello-World")
+      .setParam(PARAM_REPOSITORY_KEY, "octocat/" + PROJECT_KEY_NAME)
       .executeProtobuf(Projects.CreateWsResponse.class);
 
     Projects.CreateWsResponse.Project result = response.getProject();
-    assertThat(result.getKey()).isEqualTo(repository.getFullName().replace("/", "_"));
+    assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
     assertThat(result.getName()).isEqualTo(repository.getName());
 
     Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
@@ -114,33 +117,35 @@ public class ImportGithubProjectActionTest {
   }
 
   @Test
-  public void fail_project_already_exist() {
+  public void importProject_ifProjectWithSameNameAlreadyExists_importSucceed() {
     AlmSettingDto githubAlmSetting = setupAlm();
     db.almPats().insert(p -> p.setAlmSettingUuid(githubAlmSetting.getUuid()).setUserUuid(userSession.getUuid()));
-    db.components().insertPublicProject(p -> p.setDbKey("octocat_Hello-World"));
+    db.components().insertPublicProject(p -> p.setDbKey("Hello-World"));
 
-    GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, "Hello-World", false, "octocat/Hello-World",
+    GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, "Hello-World", false, "Hello-World",
       "https://github.sonarsource.com/api/v3/repos/octocat/Hello-World", "main");
-    when(appClient.getRepository(any(), any(), any(), any()))
-      .thenReturn(Optional.of(repository));
+    when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
+    when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
 
-    TestRequest request = ws.newRequest()
-        .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
-        .setParam(PARAM_ORGANIZATION, "octocat")
-        .setParam(PARAM_REPOSITORY_KEY, "octocat/Hello-World");
-    assertThatThrownBy(request::execute)
-        .isInstanceOf(BadRequestException.class)
-        .hasMessage("Could not create null, key already exists: octocat_Hello-World");
+    Projects.CreateWsResponse response = ws.newRequest()
+      .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
+      .setParam(PARAM_ORGANIZATION, "octocat")
+      .setParam(PARAM_REPOSITORY_KEY, "Hello-World")
+      .executeProtobuf(Projects.CreateWsResponse.class);
+
+    Projects.CreateWsResponse.Project result = response.getProject();
+    assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
+    assertThat(result.getName()).isEqualTo(repository.getName());
   }
 
   @Test
   public void fail_when_not_logged_in() {
     TestRequest request = ws.newRequest()
-        .setParam(PARAM_ALM_SETTING, "asdfghjkl")
-        .setParam(PARAM_ORGANIZATION, "test")
-        .setParam(PARAM_REPOSITORY_KEY, "test/repo");
+      .setParam(PARAM_ALM_SETTING, "asdfghjkl")
+      .setParam(PARAM_ORGANIZATION, "test")
+      .setParam(PARAM_REPOSITORY_KEY, "test/repo");
     assertThatThrownBy(request::execute)
-        .isInstanceOf(UnauthorizedException.class);
+      .isInstanceOf(UnauthorizedException.class);
   }
 
   @Test
@@ -156,12 +161,12 @@ public class ImportGithubProjectActionTest {
     userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS);
 
     TestRequest request = ws.newRequest()
-        .setParam(PARAM_ALM_SETTING, "unknown")
-        .setParam(PARAM_ORGANIZATION, "test")
-        .setParam(PARAM_REPOSITORY_KEY, "test/repo");
+      .setParam(PARAM_ALM_SETTING, "unknown")
+      .setParam(PARAM_ORGANIZATION, "test")
+      .setParam(PARAM_REPOSITORY_KEY, "test/repo");
     assertThatThrownBy(request::execute)
-        .isInstanceOf(NotFoundException.class)
-        .hasMessage("ALM Setting 'unknown' not found");
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage("ALM Setting 'unknown' not found");
   }
 
   @Test
@@ -169,12 +174,12 @@ public class ImportGithubProjectActionTest {
     AlmSettingDto githubAlmSetting = setupAlm();
 
     TestRequest request = ws.newRequest()
-        .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
-        .setParam(PARAM_ORGANIZATION, "test")
-        .setParam(PARAM_REPOSITORY_KEY, "test/repo");
+      .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
+      .setParam(PARAM_ORGANIZATION, "test")
+      .setParam(PARAM_REPOSITORY_KEY, "test/repo");
     assertThatThrownBy(request::execute)
-        .isInstanceOf(IllegalArgumentException.class)
-        .hasMessage("No personal access token found");
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("No personal access token found");
   }
 
   @Test
index 62487cca25b5f9aff50a6eac8a6170fb987668ef..fa1be6a1219dc595cc2ae644d37c3c7f31387faf 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.server.almintegration.ws.gitlab;
 
 import java.util.Optional;
-import java.util.stream.IntStream;
 import org.assertj.core.api.Assertions;
 import org.junit.Before;
 import org.junit.Rule;
@@ -31,13 +30,13 @@ import org.sonar.alm.client.gitlab.Project;
 import org.sonar.api.utils.System2;
 import org.sonar.core.i18n.I18n;
 import org.sonar.core.util.SequenceUuidFactory;
-import org.sonar.core.util.UuidFactory;
 import org.sonar.db.DbTester;
 import org.sonar.db.alm.setting.AlmSettingDto;
 import org.sonar.db.component.BranchDto;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.component.ComponentUpdater;
 import org.sonar.server.es.TestProjectIndexers;
 import org.sonar.server.favorite.FavoriteUpdater;
@@ -50,7 +49,6 @@ import org.sonarqube.ws.Projects;
 
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
-import static java.util.stream.Collectors.joining;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.tuple;
@@ -63,6 +61,8 @@ import static org.sonar.server.tester.UserSessionRule.standalone;
 
 public class ImportGitLabProjectActionTest {
 
+  private static final String PROJECT_KEY_NAME = "PROJECT_NAME";
+
   private final System2 system2 = mock(System2.class);
 
   @Rule
@@ -75,11 +75,11 @@ public class ImportGitLabProjectActionTest {
     mock(PermissionTemplateService.class), new FavoriteUpdater(db.getDbClient()), new TestProjectIndexers(), new SequenceUuidFactory());
 
   private final GitlabHttpClient gitlabHttpClient = mock(GitlabHttpClient.class);
-  private final UuidFactory uuidFactory = mock(UuidFactory.class);
   private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession);
   private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class);
+  private final ProjectKeyGenerator projectKeyGenerator = mock(ProjectKeyGenerator.class);
   private final ImportGitLabProjectAction importGitLabProjectAction = new ImportGitLabProjectAction(
-    db.getDbClient(), userSession, projectDefaultVisibility, gitlabHttpClient, componentUpdater, uuidFactory, importHelper);
+    db.getDbClient(), userSession, projectDefaultVisibility, gitlabHttpClient, componentUpdater, importHelper, projectKeyGenerator);
   private final WsActionTester ws = new WsActionTester(importGitLabProjectAction);
 
   @Before
@@ -100,7 +100,7 @@ public class ImportGitLabProjectActionTest {
     Project project = getGitlabProject();
     when(gitlabHttpClient.getProject(any(), any(), any())).thenReturn(project);
     when(gitlabHttpClient.getBranches(any(), any(), any())).thenReturn(singletonList(new GitLabBranch("master", true)));
-    when(uuidFactory.create()).thenReturn("uuid");
+    when(projectKeyGenerator.generateUniqueProjectKey(project.getPathWithNamespace())).thenReturn(PROJECT_KEY_NAME);
 
     Projects.CreateWsResponse response = ws.newRequest()
       .setParam("almSetting", almSetting.getKey())
@@ -110,7 +110,7 @@ public class ImportGitLabProjectActionTest {
     verify(gitlabHttpClient).getProject(almSetting.getUrl(), "PAT", 12345L);
 
     Projects.CreateWsResponse.Project result = response.getProject();
-    assertThat(result.getKey()).isEqualTo(project.getPathWithNamespace() + "_uuid");
+    assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
     assertThat(result.getName()).isEqualTo(project.getName());
 
     Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
@@ -131,7 +131,7 @@ public class ImportGitLabProjectActionTest {
     Project project = getGitlabProject();
     when(gitlabHttpClient.getProject(any(), any(), any())).thenReturn(project);
     when(gitlabHttpClient.getBranches(any(), any(), any())).thenReturn(singletonList(new GitLabBranch("main", true)));
-    when(uuidFactory.create()).thenReturn("uuid");
+    when(projectKeyGenerator.generateUniqueProjectKey(project.getPathWithNamespace())).thenReturn(PROJECT_KEY_NAME);
 
     Projects.CreateWsResponse response = ws.newRequest()
       .setParam("almSetting", almSetting.getKey())
@@ -142,7 +142,7 @@ public class ImportGitLabProjectActionTest {
     verify(gitlabHttpClient).getBranches(almSetting.getUrl(), "PAT", 12345L);
 
     Projects.CreateWsResponse.Project result = response.getProject();
-    assertThat(result.getKey()).isEqualTo(project.getPathWithNamespace() + "_uuid");
+    assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
     assertThat(result.getName()).isEqualTo(project.getName());
 
     Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
@@ -167,7 +167,7 @@ public class ImportGitLabProjectActionTest {
     Project project = getGitlabProject();
     when(gitlabHttpClient.getProject(any(), any(), any())).thenReturn(project);
     when(gitlabHttpClient.getBranches(any(), any(), any())).thenReturn(emptyList());
-    when(uuidFactory.create()).thenReturn("uuid");
+    when(projectKeyGenerator.generateUniqueProjectKey(project.getPathWithNamespace())).thenReturn(PROJECT_KEY_NAME);
 
     Projects.CreateWsResponse response = ws.newRequest()
       .setParam("almSetting", almSetting.getKey())
@@ -178,7 +178,7 @@ public class ImportGitLabProjectActionTest {
     verify(gitlabHttpClient).getBranches(almSetting.getUrl(), "PAT", 12345L);
 
     Projects.CreateWsResponse.Project result = response.getProject();
-    assertThat(result.getKey()).isEqualTo(project.getPathWithNamespace() + "_uuid");
+    assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
     assertThat(result.getName()).isEqualTo(project.getName());
 
     Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
@@ -190,36 +190,6 @@ public class ImportGitLabProjectActionTest {
       .containsExactlyInAnyOrder(tuple("master", true));
   }
 
-  @Test
-  public void generate_project_key_less_than_250() {
-    String name = "abcdeert";
-    assertThat(importGitLabProjectAction.generateProjectKey(name, "uuid")).isEqualTo("abcdeert_uuid");
-  }
-
-  @Test
-  public void generate_project_key_equal_250() {
-    String name = IntStream.range(0, 245).mapToObj(i -> "a").collect(joining());
-    String projectKey = importGitLabProjectAction.generateProjectKey(name, "uuid");
-    assertThat(projectKey)
-      .hasSize(250)
-      .isEqualTo(name + "_uuid");
-
-  }
-
-  @Test
-  public void generate_project_key_more_than_250() {
-    String name = IntStream.range(0, 250).mapToObj(i -> "a").collect(joining());
-    String projectKey = importGitLabProjectAction.generateProjectKey(name, "uuid");
-    assertThat(projectKey)
-      .hasSize(250)
-      .isEqualTo(name.substring(5) + "_uuid");
-  }
-
-  @Test
-  public void generate_project_key_containing_slash() {
-    String name = "a/b/c";
-    assertThat(importGitLabProjectAction.generateProjectKey(name, "uuid")).isEqualTo("a_b_c_uuid");
-  }
 
   private Project getGitlabProject() {
     return new Project(randomAlphanumeric(5), randomAlphanumeric(5));
index dc63789074f992cd399a22f67c934a4145d0e976..cded3afbfc020966863c5e30a5d09c5c6f2cb154 100644 (file)
@@ -56,6 +56,7 @@ import org.sonar.core.platform.SpringComponentContainer;
 import org.sonar.server.almintegration.ws.AlmIntegrationsWSModule;
 import org.sonar.server.almintegration.ws.CredentialsEncoderHelper;
 import org.sonar.server.almintegration.ws.ImportHelper;
+import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
 import org.sonar.server.almsettings.MultipleAlmFeatureProvider;
 import org.sonar.server.almsettings.ws.AlmSettingsWsModule;
 import org.sonar.server.authentication.AuthenticationModule;
@@ -526,6 +527,7 @@ public class PlatformLevel4 extends PlatformLevel {
       TimeoutConfigurationImpl.class,
       CredentialsEncoderHelper.class,
       ImportHelper.class,
+      ProjectKeyGenerator.class,
       GithubAppSecurityImpl.class,
       GithubApplicationClientImpl.class,
       GithubApplicationHttpClientImpl.class,
index ab786773ea9ddc9d131c312f332018260b4c51ce..73dc84dacc7cfebf8a2536f8e7fdc9dee6160f00 100644 (file)
@@ -34,11 +34,19 @@ public final class ComponentKeys {
   public static final String MALFORMED_KEY_MESSAGE = "Malformed key for '%s'. %s.";
 
   /**
-   * Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit
+   * Allowed characters are alphanumeric, '-', '_', '.' and ':'
    */
-  private static final Pattern VALID_PROJECT_KEY_REGEXP = Pattern.compile("[\\p{Alnum}\\-_.:]*[\\p{Alpha}\\-_.:]+[\\p{Alnum}\\-_.:]*");
+  private static final String VALID_PROJECT_KEY_CHARS = "\\p{Alnum}-_.:";
+
+  private static final Pattern INVALID_PROJECT_KEY_REGEXP = Pattern.compile("[^" + VALID_PROJECT_KEY_CHARS + "]");
+
+  /**
+   * At least one non-digit is necessary
+   */
+  private static final Pattern VALID_PROJECT_KEY_REGEXP = Pattern.compile("[" + VALID_PROJECT_KEY_CHARS + "]*[\\p{Alpha}\\-_.:]+[" + VALID_PROJECT_KEY_CHARS + "]*");
 
   private static final String KEY_WITH_BRANCH_FORMAT = "%s:%s";
+  private static final String REPLACEMENT_CHARACTER = "_";
 
   private ComponentKeys() {
     // only static stuff
@@ -66,6 +74,10 @@ public final class ComponentKeys {
     checkArgument(isValidProjectKey(keyCandidate), MALFORMED_KEY_MESSAGE, keyCandidate, ALLOWED_CHARACTERS_MESSAGE);
   }
 
+  public static String sanitizeProjectKey(String rawProjectKey) {
+    return INVALID_PROJECT_KEY_REGEXP.matcher(rawProjectKey).replaceAll(REPLACEMENT_CHARACTER);
+  }
+
   /**
    * Return the project key with potential branch
    */
diff --git a/sonar-core/src/test/java/org/sonar/core/component/ComponentKeysSanitizationTest.java b/sonar-core/src/test/java/org/sonar/core/component/ComponentKeysSanitizationTest.java
new file mode 100644 (file)
index 0000000..e8699f4
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.core.component;
+
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(Parameterized.class)
+public class ComponentKeysSanitizationTest {
+
+  @Parameterized.Parameters(name = "{index}: input {0}, expected output {1}")
+  public static Collection<String[]> data() {
+    return Arrays.asList(new String[][] {
+      {"/a/b/c/", "_a_b_c_"},
+      {".a.b:c:", ".a.b:c:"},
+      {"_1_2_3_", "_1_2_3_"},
+      {"fully_valid_-name2", "fully_valid_-name2"},
+      {"°+\"*ç%&\\/()=?`^“#Ç[]|{}≠¿ ~", "___________________________"},
+    });
+  }
+
+  private final String inputString;
+  private final String expectedOutputString;
+
+  public ComponentKeysSanitizationTest(String inputString, String expectedOutputString) {
+    this.inputString = inputString;
+    this.expectedOutputString = expectedOutputString;
+  }
+
+  @Test
+  public void sanitizeProjectKey() {
+    assertThat(ComponentKeys.sanitizeProjectKey(inputString)).isEqualTo(expectedOutputString);
+  }
+
+}