]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21819 Add DevOpsPlatformCreator for Azure DevOps.
authorWojtek Wajerowicz <115081248+wojciech-wajerowicz-sonarsource@users.noreply.github.com>
Tue, 26 Mar 2024 10:50:36 +0000 (11:50 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 28 Mar 2024 20:02:50 +0000 (20:02 +0000)
server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreator.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactory.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/package-info.java [new file with mode: 0644]
server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactoryTest.java [new file with mode: 0644]
server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorTest.java [new file with mode: 0644]
server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorTest.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/BoundProjectCreateRestRequest.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectAction.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java

diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreator.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreator.java
new file mode 100644 (file)
index 0000000..5fff071
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.common.almsettings.azuredevops;
+
+import java.util.Optional;
+import org.jetbrains.annotations.Nullable;
+import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
+import org.sonar.alm.client.azure.AzureDevopsServerException;
+import org.sonar.alm.client.azure.GsonAzureRepo;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.pat.AlmPatDto;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.project.CreationMethod;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.user.UserSession;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+public class AzureDevOpsProjectCreator implements DevOpsProjectCreator {
+
+  private final DbClient dbClient;
+  private final AlmSettingDto almSettingDto;
+  private final DevOpsProjectDescriptor devOpsProjectDescriptor;
+  private final UserSession userSession;
+  private final AzureDevOpsHttpClient azureDevOpsHttpClient;
+  private final ProjectCreator projectCreator;
+  private final ProjectKeyGenerator projectKeyGenerator;
+
+  public AzureDevOpsProjectCreator(DbClient dbClient, AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor, UserSession userSession,
+    AzureDevOpsHttpClient azureDevOpsHttpClient, ProjectCreator projectCreator, ProjectKeyGenerator projectKeyGenerator) {
+    this.dbClient = dbClient;
+    this.almSettingDto = almSettingDto;
+    this.devOpsProjectDescriptor = devOpsProjectDescriptor;
+    this.userSession = userSession;
+    this.azureDevOpsHttpClient = azureDevOpsHttpClient;
+    this.projectCreator = projectCreator;
+    this.projectKeyGenerator = projectKeyGenerator;
+  }
+
+  @Override
+  public boolean isScanAllowedUsingPermissionsFromDevopsPlatform() {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+
+  @Override
+  public ComponentCreationData createProjectAndBindToDevOpsPlatform(DbSession dbSession, CreationMethod creationMethod, Boolean monorepo, @Nullable String projectKey,
+    @Nullable String projectName) {
+    String pat = findPersonalAccessTokenOrThrow(dbSession, almSettingDto);
+    String url = requireNonNull(almSettingDto.getUrl(), "DevOps Platform url cannot be null");
+    checkArgument(devOpsProjectDescriptor.projectIdentifier() != null, "DevOps Project Identifier cannot be null for Azure DevOps");
+    GsonAzureRepo repo = fetchAzureDevOpsProject(url, pat, devOpsProjectDescriptor.projectIdentifier(), devOpsProjectDescriptor.repositoryIdentifier());
+
+    ComponentCreationData componentCreationData = projectCreator.createProject(
+      dbSession,
+      getProjectKey(projectKey, repo),
+      getProjectName(projectName, repo),
+      repo.getDefaultBranchName(),
+      creationMethod);
+    ProjectDto projectDto = Optional.ofNullable(componentCreationData.projectDto()).orElseThrow();
+    createProjectAlmSettingDto(dbSession, repo, projectDto, almSettingDto, monorepo);
+    return componentCreationData;
+  }
+
+  private String findPersonalAccessTokenOrThrow(DbSession dbSession, AlmSettingDto almSettingDto) {
+    String userUuid = requireNonNull(userSession.getUuid(), "User UUID cannot be null.");
+    Optional<AlmPatDto> almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto);
+    return almPatDto.map(AlmPatDto::getPersonalAccessToken)
+      .orElseThrow(() -> new IllegalArgumentException(String.format("personal access token for '%s' is missing", almSettingDto.getKey())));
+  }
+
+  private GsonAzureRepo fetchAzureDevOpsProject(String azureDevOpsUrl, String pat, String projectIdentifier, String repositoryIdentifier) {
+    try {
+      return azureDevOpsHttpClient.getRepo(azureDevOpsUrl, pat, projectIdentifier, repositoryIdentifier);
+    } catch (AzureDevopsServerException e) {
+      throw new IllegalStateException(format("Failed to fetch AzureDevOps repository '%s' from project '%s' from '%s'", repositoryIdentifier, projectIdentifier, azureDevOpsUrl),
+        e);
+    }
+  }
+
+  private String getProjectKey(@Nullable String projectKey, GsonAzureRepo repository) {
+    return Optional.ofNullable(projectKey).orElseGet(() -> projectKeyGenerator.generateUniqueProjectKey(repository.getProject().getName(), repository.getName()));
+  }
+
+  private static String getProjectName(@Nullable String projectName, GsonAzureRepo repository) {
+    return Optional.ofNullable(projectName).orElse(repository.getName());
+  }
+
+  private void createProjectAlmSettingDto(DbSession dbSession, GsonAzureRepo repository, ProjectDto projectDto, AlmSettingDto almSettingDto, Boolean monorepo) {
+    ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto()
+      .setAlmSettingUuid(almSettingDto.getUuid())
+      .setAlmRepo(repository.getName())
+      .setAlmSlug(repository.getProject().getName())
+      .setProjectUuid(projectDto.getUuid())
+      .setSummaryCommentEnabled(true)
+      .setMonorepo(monorepo);
+    dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, projectAlmSettingDto, almSettingDto.getKey(), projectDto.getName(), projectDto.getKey());
+  }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactory.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactory.java
new file mode 100644 (file)
index 0000000..e6ab434
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.common.almsettings.azuredevops;
+
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreatorFactory;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.user.UserSession;
+
+public class AzureDevOpsProjectCreatorFactory implements DevOpsProjectCreatorFactory {
+
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final AzureDevOpsHttpClient azureDevOpsHttpClient;
+  private final ProjectCreator projectCreator;
+  private final ProjectKeyGenerator projectKeyGenerator;
+
+  public AzureDevOpsProjectCreatorFactory(DbClient dbClient, UserSession userSession, AzureDevOpsHttpClient azureDevOpsHttpClient, ProjectCreator projectCreator,
+    ProjectKeyGenerator projectKeyGenerator) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+    this.azureDevOpsHttpClient = azureDevOpsHttpClient;
+    this.projectCreator = projectCreator;
+    this.projectKeyGenerator = projectKeyGenerator;
+  }
+
+  @Override
+  public Optional<DevOpsProjectCreator> getDevOpsProjectCreator(DbSession dbSession, Map<String, String> characteristics) {
+    return Optional.empty();
+  }
+
+  @Override
+  public Optional<DevOpsProjectCreator> getDevOpsProjectCreator(AlmSettingDto almSettingDto, DevOpsProjectDescriptor devOpsProjectDescriptor) {
+    if (almSettingDto.getAlm() != ALM.AZURE_DEVOPS) {
+      return Optional.empty();
+    }
+    return Optional.of(new AzureDevOpsProjectCreator(dbClient, almSettingDto, devOpsProjectDescriptor, userSession, azureDevOpsHttpClient, projectCreator, projectKeyGenerator));
+  }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/package-info.java
new file mode 100644 (file)
index 0000000..a8bcc6b
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.common.almsettings.azuredevops;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactoryTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactoryTest.java
new file mode 100644 (file)
index 0000000..41b9066
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.common.almsettings.azuredevops;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
+import org.sonar.db.DbClient;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreator;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.user.UserSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AzureDevOpsProjectCreatorFactoryTest {
+
+  @Mock()
+  private DbClient dbClient;
+
+  @Mock
+  private UserSession userSession;
+  @Mock
+  private AzureDevOpsHttpClient azureDevOpsHttpClient;
+  @Mock
+  private ProjectCreator projectCreator;
+  @Mock
+  private ProjectKeyGenerator projectKeyGenerator;
+
+  @InjectMocks
+  private AzureDevOpsProjectCreatorFactory underTest;
+
+  @Test
+  void getDevOpsProjectCreator_whenAlmIsNotAzureDevOps_shouldReturnEmpty() {
+    AlmSettingDto almSettingDto = mock(AlmSettingDto.class);
+    when(almSettingDto.getAlm()).thenReturn(ALM.GITLAB);
+    DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.GITLAB, null, null, "bitbucket_project");
+    assertThat(underTest.getDevOpsProjectCreator(almSettingDto, devOpsProjectDescriptor)).isEmpty();
+  }
+
+  @Test
+  void getDevOpsProjectCreator_whenAlmIsAzureDevOps_shouldReturnProjectCreator() {
+    AlmSettingDto almSettingDto = mock(AlmSettingDto.class);
+    when(almSettingDto.getAlm()).thenReturn(ALM.AZURE_DEVOPS);
+    DevOpsProjectDescriptor devOpsProjectDescriptor = new DevOpsProjectDescriptor(ALM.AZURE_DEVOPS, null, "project-identifier", "bitbucket_project");
+
+    DevOpsProjectCreator expectedProjectCreator = new AzureDevOpsProjectCreator(dbClient, almSettingDto, devOpsProjectDescriptor, userSession, azureDevOpsHttpClient,
+      projectCreator, projectKeyGenerator);
+    DevOpsProjectCreator devOpsProjectCreator = underTest.getDevOpsProjectCreator(almSettingDto, devOpsProjectDescriptor).orElseThrow();
+
+    assertThat(devOpsProjectCreator).usingRecursiveComparison().isEqualTo(expectedProjectCreator);
+  }
+
+}
diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorTest.java
new file mode 100644 (file)
index 0000000..436cd64
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.common.almsettings.azuredevops;
+
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
+import org.sonar.alm.client.azure.AzureDevopsServerException;
+import org.sonar.alm.client.azure.GsonAzureRepo;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.pat.AlmPatDto;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+import org.sonar.db.alm.setting.ProjectAlmSettingDto;
+import org.sonar.db.project.CreationMethod;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
+import org.sonar.server.common.project.ProjectCreator;
+import org.sonar.server.component.ComponentCreationData;
+import org.sonar.server.user.UserSession;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AzureDevOpsProjectCreatorTest {
+
+  private static final String USER_LOGIN = "userLogin";
+  private static final String USER_UUID = "userUuid";
+  private static final String REPOSITORY_NAME = "repositoryName";
+  private static final String DEVOPS_PROJECT_ID = "project-identifier";
+  private static final String DEVOPS_PROJECT_NAME = "devops-project-name";
+  private static final String ALM_SETTING_KEY = "azuredevops_config_1";
+  private static final String ALM_SETTING_UUID = "almSettingUuid";
+  private static final String USER_PAT = "1234";
+  public static final String AZURE_DEVOPS_URL = "http://api.com";
+  private static final String MAIN_BRANCH_NAME = "defaultBranch";
+  private static final String PROJECT_UUID = "projectUuid";
+
+  @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+  private DbClient dbClient;
+  @Mock
+  private AlmSettingDto almSettingDto;
+  @Mock
+  private DevOpsProjectDescriptor devOpsProjectDescriptor;
+  @Mock
+  private UserSession userSession;
+  @Mock
+  private AzureDevOpsHttpClient azureDevOpsHttpClient;
+  @Mock
+  private ProjectCreator projectCreator;
+  @Mock
+  private ProjectKeyGenerator projectKeyGenerator;
+
+  @InjectMocks
+  private AzureDevOpsProjectCreator underTest;
+
+  @BeforeEach
+  void setup() {
+    lenient().when(userSession.getLogin()).thenReturn(USER_LOGIN);
+    lenient().when(userSession.getUuid()).thenReturn(USER_UUID);
+
+    lenient().when(almSettingDto.getKey()).thenReturn(ALM_SETTING_KEY);
+    lenient().when(almSettingDto.getUuid()).thenReturn(ALM_SETTING_UUID);
+    lenient().when(almSettingDto.getUrl()).thenReturn(AZURE_DEVOPS_URL);
+
+    lenient().when(devOpsProjectDescriptor.repositoryIdentifier()).thenReturn(REPOSITORY_NAME);
+    lenient().when(devOpsProjectDescriptor.projectIdentifier()).thenReturn(DEVOPS_PROJECT_ID);
+    lenient().when(devOpsProjectDescriptor.alm()).thenReturn(ALM.BITBUCKET_CLOUD);
+  }
+
+  @Test
+  void isScanAllowedUsingPermissionsFromDevopsPlatform_shouldThrowUnsupportedOperationException() {
+    assertThatExceptionOfType(UnsupportedOperationException.class)
+      .isThrownBy(() -> underTest.isScanAllowedUsingPermissionsFromDevopsPlatform())
+      .withMessage("Not Implemented");
+  }
+
+  @Test
+  void createProjectAndBindToDevOpsPlatform_whenPatIsMissing_shouldThrow() {
+    assertThatExceptionOfType(IllegalArgumentException.class)
+      .isThrownBy(() -> underTest.createProjectAndBindToDevOpsPlatform(mock(DbSession.class), CreationMethod.ALM_IMPORT_API, false, null, null))
+      .withMessage("personal access token for 'azuredevops_config_1' is missing");
+  }
+
+  @Test
+  void createProjectAndBindToDevOpsPlatform_whenRepositoryNotFound_shouldThrow() {
+    mockPatForUser();
+    when(azureDevOpsHttpClient.getRepo(AZURE_DEVOPS_URL, USER_PAT, DEVOPS_PROJECT_ID, REPOSITORY_NAME))
+      .thenThrow(new AzureDevopsServerException(404, "Problem fetching repository from AzureDevOps"));
+    assertThatExceptionOfType(IllegalStateException.class)
+      .isThrownBy(() -> underTest.createProjectAndBindToDevOpsPlatform(mock(DbSession.class), CreationMethod.ALM_IMPORT_API, false, null, null))
+      .withMessage("Failed to fetch AzureDevOps repository 'repositoryName' from project 'project-identifier' from 'http://api.com'");
+  }
+
+  @Test
+  void createProjectAndBindToDevOpsPlatform_projectIdentifierIsNull_shouldThrow() {
+    mockPatForUser();
+    lenient().when(devOpsProjectDescriptor.projectIdentifier()).thenReturn(null);
+
+    assertThatExceptionOfType(IllegalArgumentException.class)
+      .isThrownBy(() -> underTest.createProjectAndBindToDevOpsPlatform(mock(DbSession.class), CreationMethod.ALM_IMPORT_API, false, null, null))
+      .withMessage("DevOps Project Identifier cannot be null for Azure DevOps");
+  }
+
+  @Test
+  void createProjectAndBindToDevOpsPlatform_whenRepoFoundOnAzureDevOps_successfullyCreatesProject() {
+    mockPatForUser();
+    mockAzureDevOpsProject();
+
+    mockProjectCreation("projectKey", "projectName");
+
+    underTest.createProjectAndBindToDevOpsPlatform(mock(DbSession.class), CreationMethod.ALM_IMPORT_API, true, "projectKey", "projectName");
+
+    ArgumentCaptor<ProjectAlmSettingDto> projectAlmSettingCaptor = ArgumentCaptor.forClass(ProjectAlmSettingDto.class);
+
+    verify(dbClient.projectAlmSettingDao()).insertOrUpdate(any(), projectAlmSettingCaptor.capture(), eq(ALM_SETTING_KEY), eq("projectName"), eq("projectKey"));
+
+    ProjectAlmSettingDto createdProjectAlmSettingDto = projectAlmSettingCaptor.getValue();
+
+    assertThat(createdProjectAlmSettingDto.getAlmSettingUuid()).isEqualTo(ALM_SETTING_UUID);
+    assertThat(createdProjectAlmSettingDto.getAlmRepo()).isEqualTo(REPOSITORY_NAME);
+    assertThat(createdProjectAlmSettingDto.getAlmSlug()).isEqualTo(DEVOPS_PROJECT_NAME);
+    assertThat(createdProjectAlmSettingDto.getProjectUuid()).isEqualTo(PROJECT_UUID);
+    assertThat(createdProjectAlmSettingDto.getMonorepo()).isTrue();
+  }
+
+  @Test
+  void createProjectAndBindToDevOpsPlatform_whenNoKeyAndNameSpecified_generatesKeyAndUsesAzureRepositoryName() {
+    mockPatForUser();
+    mockAzureDevOpsProject();
+
+
+    String generatedProjectKey = "generatedProjectKey";
+    when(projectKeyGenerator.generateUniqueProjectKey(DEVOPS_PROJECT_NAME, REPOSITORY_NAME)).thenReturn(generatedProjectKey);
+
+    mockProjectCreation(generatedProjectKey, REPOSITORY_NAME);
+
+    underTest.createProjectAndBindToDevOpsPlatform(mock(DbSession.class), CreationMethod.ALM_IMPORT_API, true, null, null);
+
+    ArgumentCaptor<ProjectAlmSettingDto> projectAlmSettingCaptor = ArgumentCaptor.forClass(ProjectAlmSettingDto.class);
+
+    verify(dbClient.projectAlmSettingDao()).insertOrUpdate(any(), projectAlmSettingCaptor.capture(), eq(ALM_SETTING_KEY), eq(REPOSITORY_NAME), eq(generatedProjectKey));
+
+    ProjectAlmSettingDto createdProjectAlmSettingDto = projectAlmSettingCaptor.getValue();
+
+    assertThat(createdProjectAlmSettingDto.getAlmSettingUuid()).isEqualTo(ALM_SETTING_UUID);
+    assertThat(createdProjectAlmSettingDto.getAlmRepo()).isEqualTo(REPOSITORY_NAME);
+    assertThat(createdProjectAlmSettingDto.getAlmSlug()).isEqualTo(DEVOPS_PROJECT_NAME);
+    assertThat(createdProjectAlmSettingDto.getProjectUuid()).isEqualTo(PROJECT_UUID);
+    assertThat(createdProjectAlmSettingDto.getMonorepo()).isTrue();
+  }
+
+  private void mockPatForUser() {
+    AlmPatDto almPatDto = mock();
+    when(almPatDto.getPersonalAccessToken()).thenReturn(USER_PAT);
+    when(dbClient.almPatDao().selectByUserAndAlmSetting(any(), eq(USER_UUID), eq(almSettingDto))).thenReturn(Optional.of(almPatDto));
+  }
+
+  private void mockAzureDevOpsProject() {
+    GsonAzureRepo repository = mock(GsonAzureRepo.class, Answers.RETURNS_DEEP_STUBS);
+    when(repository.getName()).thenReturn(REPOSITORY_NAME);
+    when(repository.getDefaultBranchName()).thenReturn(MAIN_BRANCH_NAME);
+    when(repository.getProject().getName()).thenReturn(DEVOPS_PROJECT_NAME);
+    when(azureDevOpsHttpClient.getRepo(AZURE_DEVOPS_URL, USER_PAT, DEVOPS_PROJECT_ID, REPOSITORY_NAME)).thenReturn(repository);
+  }
+
+  private void mockProjectCreation(String projectKey, String projectName) {
+    ComponentCreationData componentCreationData = mock();
+    ProjectDto projectDto = mock();
+    when(componentCreationData.projectDto()).thenReturn(projectDto);
+    when(projectDto.getUuid()).thenReturn(PROJECT_UUID);
+    when(projectDto.getKey()).thenReturn(projectKey);
+    when(projectDto.getName()).thenReturn(projectName);
+    when(projectCreator.createProject(any(), eq(projectKey), eq(projectName), eq(MAIN_BRANCH_NAME), eq(CreationMethod.ALM_IMPORT_API)))
+      .thenReturn(componentCreationData);
+  }
+
+}
index c58fd39e64bebf3b262d57da128c4cd66be8bff6..1e67da2ff7cf6430f5a5cc1e85b2ab64e890f718 100644 (file)
@@ -60,24 +60,17 @@ import static org.mockito.Mockito.when;
 class GitlabProjectCreatorTest {
 
   private static final String PROJECT_UUID = "projectUuid";
-
   private static final String USER_LOGIN = "userLogin";
   private static final String USER_UUID = "userUuid";
-
   private static final String REPOSITORY_PATH_WITH_NAMESPACE = "pathWith/namespace";
-
   private static final String GITLAB_PROJECT_NAME = "gitlabProjectName";
-
   private static final String REPOSITORY_ID = "1234";
-
   private static final String MAIN_BRANCH_NAME = "defaultBranch";
-
   private static final String ALM_SETTING_KEY = "gitlab_config_1";
   private static final String ALM_SETTING_UUID = "almSettingUuid";
-
   private static final String USER_PAT = "1234";
-
   public static final String GITLAB_URL = "http://api.com";
+
   @Mock(answer = Answers.RETURNS_DEEP_STUBS)
   private DbClient dbClient;
 
@@ -159,7 +152,7 @@ class GitlabProjectCreatorTest {
   }
 
   @Test
-  void createProjectAndBindToDevOpsPlatform_whenNoKeyAndNameSpecified_generatesKeyAndUsersGitlabProjectName() {
+  void createProjectAndBindToDevOpsPlatform_whenNoKeyAndNameSpecified_generatesKeyAndUsesGitlabProjectName() {
     mockPatForUser();
     mockGitlabProject();
     mockMainBranch();
index 74f8bdf2ef6725129786314282015b887217a55b..ce9809a3dd0d75d8513ee961ddc4d92cc425d4a1 100644 (file)
@@ -44,6 +44,7 @@ public record BoundProjectCreateRestRequest(
       Identifier of the DevOps platform repository to import:
       - repository slug for GitHub and Bitbucket (Cloud and Server)
       - repository id for GitLab
+      - repository name for Azure DevOps
       """)
   String repositoryIdentifier,
 
index f887a1d1945b1dc7d7e2b4c5814405ba0df56ca5..5ccd97862ea5d96d5c40e558d3968c92fbaf7c60 100644 (file)
@@ -42,10 +42,14 @@ import org.sonar.db.project.ProjectDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.almintegration.ws.ImportHelper;
 import org.sonar.server.common.almintegration.ProjectKeyGenerator;
+import org.sonar.server.common.almsettings.DevOpsProjectCreatorFactory;
+import org.sonar.server.common.almsettings.azuredevops.AzureDevOpsProjectCreatorFactory;
 import org.sonar.server.common.component.ComponentUpdater;
 import org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver;
 import org.sonar.server.common.permission.PermissionTemplateService;
 import org.sonar.server.common.permission.PermissionUpdater;
+import org.sonar.server.common.project.ImportProjectService;
+import org.sonar.server.common.project.ProjectCreator;
 import org.sonar.server.es.TestIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
@@ -103,11 +107,16 @@ public class ImportAzureProjectActionIT {
   private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class);
   private final ProjectKeyGenerator projectKeyGenerator = mock(ProjectKeyGenerator.class);
 
-  private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
-  private NewCodeDefinitionResolver newCodeDefinitionResolver = new NewCodeDefinitionResolver(db.getDbClient(), editionProvider);
-  private final ImportAzureProjectAction importAzureProjectAction = new ImportAzureProjectAction(db.getDbClient(), userSession,
-    azureDevOpsHttpClient, projectDefaultVisibility, componentUpdater, importHelper, projectKeyGenerator, newCodeDefinitionResolver,
-    defaultBranchNameResolver);
+  private final PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
+  private final NewCodeDefinitionResolver newCodeDefinitionResolver = new NewCodeDefinitionResolver(db.getDbClient(), editionProvider);
+  private final ProjectCreator projectCreator = new ProjectCreator(userSession, projectDefaultVisibility, componentUpdater);
+  private final DevOpsProjectCreatorFactory devOpsProjectCreatorFactory = new AzureDevOpsProjectCreatorFactory(db.getDbClient(), userSession, azureDevOpsHttpClient, projectCreator,
+    projectKeyGenerator);
+
+  private final ImportProjectService importProjectService = new ImportProjectService(db.getDbClient(), devOpsProjectCreatorFactory, userSession, componentUpdater,
+    newCodeDefinitionResolver);
+  private final ImportAzureProjectAction importAzureProjectAction = new ImportAzureProjectAction(
+    importHelper, importProjectService);
   private final WsActionTester ws = new WsActionTester(importAzureProjectAction);
 
   @Before
@@ -126,7 +135,7 @@ public class ImportAzureProjectActionIT {
 
     ProjectDto projectDto = getProjectDto(result);
 
-    Optional<ProjectAlmSettingDto> projectAlmSettingDto = db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(),      projectDto);
+    Optional<ProjectAlmSettingDto> projectAlmSettingDto = db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto);
 
     assertThat(projectAlmSettingDto.get().getAlmRepo()).isEqualTo("repo-name");
     assertThat(projectAlmSettingDto.get().getAlmSettingUuid()).isEqualTo(almSetting.getUuid());
@@ -447,7 +456,6 @@ public class ImportAzureProjectActionIT {
     assertThatNoException().isThrownBy(request::execute);
   }
 
-
   @Test
   public void define() {
     WebService.Action def = ws.getDef();
index bb24cb25b4e440d128492fb020d3e74ddb4e4864..5b116519e604d6e455e99f9aa6ea206c11eb704b 100644 (file)
  */
 package org.sonar.server.almintegration.ws.azure;
 
-import java.util.Optional;
+import javax.annotation.Nullable;
 import javax.inject.Inject;
-import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
-import org.sonar.alm.client.azure.GsonAzureRepo;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.alm.pat.AlmPatDto;
 import org.sonar.db.alm.setting.ALM;
 import org.sonar.db.alm.setting.AlmSettingDto;
-import org.sonar.db.alm.setting.ProjectAlmSettingDto;
-import org.sonar.db.component.BranchDto;
-import org.sonar.db.project.ProjectDto;
 import org.sonar.server.almintegration.ws.AlmIntegrationsWsAction;
 import org.sonar.server.almintegration.ws.ImportHelper;
-import org.sonar.server.common.almintegration.ProjectKeyGenerator;
-import org.sonar.server.common.component.ComponentCreationParameters;
-import org.sonar.server.common.component.ComponentUpdater;
-import org.sonar.server.common.component.NewComponent;
-import org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver;
-import org.sonar.server.component.ComponentCreationData;
-import org.sonar.server.project.DefaultBranchNameResolver;
-import org.sonar.server.project.ProjectDefaultVisibility;
-import org.sonar.server.user.UserSession;
+import org.sonar.server.common.project.ImportProjectRequest;
+import org.sonar.server.common.project.ImportProjectService;
+import org.sonar.server.common.project.ImportedProject;
 import org.sonarqube.ws.Projects.CreateWsResponse;
 
-import static java.util.Objects.requireNonNull;
-import static org.sonar.api.resources.Qualifiers.PROJECT;
-import static org.sonar.db.project.CreationMethod.getCreationMethod;
-import static org.sonar.db.project.CreationMethod.Category.ALM_IMPORT;
 import static org.sonar.server.almintegration.ws.ImportHelper.PARAM_ALM_SETTING;
 import static org.sonar.server.almintegration.ws.ImportHelper.toCreateResponse;
-import static org.sonar.server.common.component.NewComponent.newComponentBuilder;
 import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.NEW_CODE_PERIOD_TYPE_DESCRIPTION_PROJECT_CREATION;
 import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.NEW_CODE_PERIOD_VALUE_DESCRIPTION_PROJECT_CREATION;
-import static org.sonar.server.common.newcodeperiod.NewCodeDefinitionResolver.checkNewCodeDefinitionParam;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_TYPE;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_VALUE;
@@ -67,30 +47,14 @@ public class ImportAzureProjectAction implements AlmIntegrationsWsAction {
   private static final String PARAM_REPOSITORY_NAME = "repositoryName";
   private static final String PARAM_PROJECT_NAME = "projectName";
 
-  private final DbClient dbClient;
-  private final UserSession userSession;
-  private final AzureDevOpsHttpClient azureDevOpsHttpClient;
-  private final ProjectDefaultVisibility projectDefaultVisibility;
-  private final ComponentUpdater componentUpdater;
   private final ImportHelper importHelper;
-  private final ProjectKeyGenerator projectKeyGenerator;
-  private final NewCodeDefinitionResolver newCodeDefinitionResolver;
-  private final DefaultBranchNameResolver defaultBranchNameResolver;
+
+  private final ImportProjectService importProjectService;
 
   @Inject
-  public ImportAzureProjectAction(DbClient dbClient, UserSession userSession, AzureDevOpsHttpClient azureDevOpsHttpClient,
-    ProjectDefaultVisibility projectDefaultVisibility, ComponentUpdater componentUpdater,
-    ImportHelper importHelper, ProjectKeyGenerator projectKeyGenerator, NewCodeDefinitionResolver newCodeDefinitionResolver,
-    DefaultBranchNameResolver defaultBranchNameResolver) {
-    this.dbClient = dbClient;
-    this.userSession = userSession;
-    this.azureDevOpsHttpClient = azureDevOpsHttpClient;
-    this.projectDefaultVisibility = projectDefaultVisibility;
-    this.componentUpdater = componentUpdater;
+  public ImportAzureProjectAction(ImportHelper importHelper, ImportProjectService importProjectService) {
     this.importHelper = importHelper;
-    this.projectKeyGenerator = projectKeyGenerator;
-    this.newCodeDefinitionResolver = newCodeDefinitionResolver;
-    this.defaultBranchNameResolver = defaultBranchNameResolver;
+    this.importProjectService = importProjectService;
   }
 
   @Override
@@ -140,74 +104,20 @@ public class ImportAzureProjectAction implements AlmIntegrationsWsAction {
   private CreateWsResponse doHandle(Request request) {
     importHelper.checkProvisionProjectPermission();
     AlmSettingDto almSettingDto = importHelper.getAlmSettingDtoForAlm(request, ALM.AZURE_DEVOPS);
-
     String newCodeDefinitionType = request.param(PARAM_NEW_CODE_DEFINITION_TYPE);
     String newCodeDefinitionValue = request.param(PARAM_NEW_CODE_DEFINITION_VALUE);
+    String projectName = request.mandatoryParam(PARAM_PROJECT_NAME);
+    String repositoryName = request.mandatoryParam(PARAM_REPOSITORY_NAME);
 
-    try (DbSession dbSession = dbClient.openSession(false)) {
-
-      String pat = getPat(dbSession, almSettingDto);
-
-      String projectName = request.mandatoryParam(PARAM_PROJECT_NAME);
-      String repositoryName = request.mandatoryParam(PARAM_REPOSITORY_NAME);
-
-      String url = requireNonNull(almSettingDto.getUrl(), "DevOps Platform url cannot be null");
-      GsonAzureRepo repo = azureDevOpsHttpClient.getRepo(url, pat, projectName, repositoryName);
-
-      ComponentCreationData componentCreationData = createProject(dbSession, repo);
-      ProjectDto projectDto = Optional.ofNullable(componentCreationData.projectDto()).orElseThrow();
-      BranchDto mainBranchDto = Optional.ofNullable(componentCreationData.mainBranchDto()).orElseThrow();
-      populatePRSetting(dbSession, repo, projectDto, almSettingDto);
-
-      checkNewCodeDefinitionParam(newCodeDefinitionType, newCodeDefinitionValue);
-
-      if (newCodeDefinitionType != null) {
-        newCodeDefinitionResolver.createNewCodeDefinition(dbSession, projectDto.getUuid(), mainBranchDto.getUuid(),
-          Optional.ofNullable(repo.getDefaultBranchName()).orElse(defaultBranchNameResolver.getEffectiveMainBranchName()),
-          newCodeDefinitionType, newCodeDefinitionValue);
-      }
-
-      componentUpdater.commitAndIndex(dbSession, componentCreationData);
-
-      return toCreateResponse(projectDto);
-    }
-  }
-
-  private String getPat(DbSession dbSession, AlmSettingDto almSettingDto) {
-    String userUuid = importHelper.getUserUuid();
-    Optional<AlmPatDto> almPatDto = dbClient.almPatDao().selectByUserAndAlmSetting(dbSession, userUuid, almSettingDto);
-    return almPatDto.map(AlmPatDto::getPersonalAccessToken)
-      .orElseThrow(() -> new IllegalArgumentException(String.format("personal access token for '%s' is missing", almSettingDto.getKey())));
-  }
+    ImportedProject importedProject = importProjectService
+      .importProject(toServiceRequest(almSettingDto, projectName, repositoryName, newCodeDefinitionType, newCodeDefinitionValue));
 
-  private ComponentCreationData createProject(DbSession dbSession, GsonAzureRepo repo) {
-    boolean visibility = projectDefaultVisibility.get(dbSession).isPrivate();
-    String uniqueProjectKey = projectKeyGenerator.generateUniqueProjectKey(repo.getProject().getName(), repo.getName());
-    NewComponent newProject = newComponentBuilder()
-      .setKey(uniqueProjectKey)
-      .setName(repo.getName())
-      .setPrivate(visibility)
-      .setQualifier(PROJECT)
-      .build();
-    ComponentCreationParameters componentCreationParameters = ComponentCreationParameters.builder()
-      .newComponent(newProject)
-      .userUuid(userSession.getUuid())
-      .userLogin(userSession.getLogin())
-      .mainBranchName(repo.getDefaultBranchName())
-      .creationMethod(getCreationMethod(ALM_IMPORT, userSession.isAuthenticatedBrowserSession()))
-      .build();
-    return componentUpdater.createWithoutCommit(dbSession, componentCreationParameters);
+    return toCreateResponse(importedProject.projectDto());
   }
 
-  private void populatePRSetting(DbSession dbSession, GsonAzureRepo repo, ProjectDto projectDto, AlmSettingDto almSettingDto) {
-    ProjectAlmSettingDto projectAlmSettingDto = new ProjectAlmSettingDto()
-      .setAlmSettingUuid(almSettingDto.getUuid())
-      .setAlmRepo(repo.getName())
-      .setAlmSlug(repo.getProject().getName())
-      .setProjectUuid(projectDto.getUuid())
-      .setMonorepo(false);
-    dbClient.projectAlmSettingDao().insertOrUpdate(dbSession, projectAlmSettingDto, almSettingDto.getKey(),
-      projectDto.getName(), projectDto.getKey());
+  private static ImportProjectRequest toServiceRequest(AlmSettingDto almSettingDto, String projectIdentifier, String repositoryIdentifier, @Nullable String newCodeDefinitionType,
+    @Nullable String newCodeDefinitionValue) {
+    return new ImportProjectRequest(null, null, almSettingDto.getUuid(), repositoryIdentifier, projectIdentifier, newCodeDefinitionType, newCodeDefinitionValue, false);
   }
 
 }
index a58f0f90fc8fe48f776931f7197b3f088e0c21cf..b8b7e924ba9103de431a32f19e073eda89e79565 100644 (file)
@@ -81,6 +81,7 @@ import org.sonar.server.ce.projectdump.ProjectExportWsModule;
 import org.sonar.server.ce.ws.CeWsModule;
 import org.sonar.server.common.almintegration.ProjectKeyGenerator;
 import org.sonar.server.common.almsettings.DelegatingDevOpsProjectCreatorFactory;
+import org.sonar.server.common.almsettings.azuredevops.AzureDevOpsProjectCreatorFactory;
 import org.sonar.server.common.almsettings.bitbucketcloud.BitbucketCloudProjectCreatorFactory;
 import org.sonar.server.common.almsettings.bitbucketserver.BitbucketServerProjectCreatorFactory;
 import org.sonar.server.common.almsettings.github.GithubProjectCreatorFactory;
@@ -574,6 +575,7 @@ public class PlatformLevel4 extends PlatformLevel {
       BitbucketCloudRestClientConfiguration.class,
       BitbucketServerRestClient.class,
       AzureDevOpsHttpClient.class,
+      AzureDevOpsProjectCreatorFactory.class,
       new AlmIntegrationsWSModule(),
       BitbucketCloudValidator.class,
       BitbucketCloudProjectCreatorFactory.class,