From 50f49a4d7a17aecc35290234dc9bdca638b21cd6 Mon Sep 17 00:00:00 2001 From: Wojtek Wajerowicz <115081248+wojciech-wajerowicz-sonarsource@users.noreply.github.com> Date: Tue, 26 Mar 2024 11:50:36 +0100 Subject: [PATCH] SONAR-21819 Add DevOpsPlatformCreator for Azure DevOps. --- .../AzureDevOpsProjectCreator.java | 124 ++++++++++ .../AzureDevOpsProjectCreatorFactory.java | 65 ++++++ .../almsettings/azuredevops/package-info.java | 23 ++ .../AzureDevOpsProjectCreatorFactoryTest.java | 80 +++++++ .../AzureDevOpsProjectCreatorTest.java | 211 ++++++++++++++++++ .../gitlab/GitlabProjectCreatorTest.java | 11 +- .../BoundProjectCreateRestRequest.java | 1 + .../ws/azure/ImportAzureProjectActionIT.java | 22 +- .../ws/azure/ImportAzureProjectAction.java | 122 ++-------- .../platformlevel/PlatformLevel4.java | 2 + 10 files changed, 539 insertions(+), 122 deletions(-) create mode 100644 server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreator.java create mode 100644 server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactory.java create mode 100644 server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/package-info.java create mode 100644 server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactoryTest.java create mode 100644 server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorTest.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 index 00000000000..5fff071f009 --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreator.java @@ -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 = 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 index 00000000000..e6ab4340bf4 --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactory.java @@ -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 getDevOpsProjectCreator(DbSession dbSession, Map characteristics) { + return Optional.empty(); + } + + @Override + public Optional 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 index 00000000000..a8bcc6b2768 --- /dev/null +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/almsettings/azuredevops/package-info.java @@ -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 index 00000000000..41b90665cee --- /dev/null +++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorFactoryTest.java @@ -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 index 00000000000..436cd640336 --- /dev/null +++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/azuredevops/AzureDevOpsProjectCreatorTest.java @@ -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 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 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); + } + +} diff --git a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorTest.java b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorTest.java index c58fd39e64b..1e67da2ff7c 100644 --- a/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorTest.java +++ b/server/sonar-webserver-common/src/test/java/org/sonar/server/common/almsettings/gitlab/GitlabProjectCreatorTest.java @@ -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(); diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/BoundProjectCreateRestRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/BoundProjectCreateRestRequest.java index 74f8bdf2ef6..ce9809a3dd0 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/BoundProjectCreateRestRequest.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/projects/request/BoundProjectCreateRestRequest.java @@ -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, diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectActionIT.java index f887a1d1945..5ccd97862ea 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectActionIT.java @@ -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 = db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto); + Optional 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(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectAction.java index bb24cb25b4e..5b116519e60 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almintegration/ws/azure/ImportAzureProjectAction.java @@ -19,45 +19,25 @@ */ 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 = 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); } } diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index a58f0f90fc8..b8b7e924ba9 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -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, -- 2.39.5