aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorAntoine Vigneau <antoine.vigneau@sonarsource.com>2023-10-10 11:10:59 +0200
committersonartech <sonartech@sonarsource.com>2023-10-20 20:02:39 +0000
commitda75e87d29efe342fa2761def532f0ea94f71cd1 (patch)
treee0982e08cd9a80fd38f28d660519b284a1bba84b /server
parent104274adf1f1d70808fec00c91b338260cbf5e23 (diff)
downloadsonarqube-da75e87d29efe342fa2761def532f0ea94f71cd1.tar.gz
sonarqube-da75e87d29efe342fa2761def532f0ea94f71cd1.zip
SONAR-20699 Infer GitHub DevOps Platform from scanner information
Diffstat (limited to 'server')
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java6
-rw-r--r--server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java8
-rw-r--r--server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java33
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java66
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java36
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsProjectDescriptor.java25
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java78
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformServiceTest.java109
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java120
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java4
10 files changed, 485 insertions, 0 deletions
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java
index 11848ba0f4a..7ecfb54acec 100644
--- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClient.java
@@ -47,6 +47,12 @@ public interface GithubApplicationClient {
GithubBinding.GsonApp getApp(GithubAppConfiguration githubAppConfiguration);
/**
+ * Retrieve the installation id for the given accountName.
+ * @throws IllegalArgumentException if one of the arguments is invalid (for example, wrong private key)
+ */
+ Optional<Long> getInstallationId(GithubAppConfiguration githubAppConfiguration, String repositorySlug);
+
+ /**
* Lists all the organizations accessible to the access token provided.
*/
Organizations listOrganizations(String appUrl, AccessToken accessToken, int page, int pageSize);
diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
index ceb5a2f93ab..6b242deba7b 100644
--- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
+++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationClientImpl.java
@@ -146,6 +146,14 @@ public class GithubApplicationClientImpl implements GithubApplicationClient {
}
@Override
+ public Optional<Long> getInstallationId(GithubAppConfiguration githubAppConfiguration, String repositorySlug) {
+ AppToken appToken = appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey());
+ String endpoint = String.format("/repos/%s/installation", repositorySlug);
+ return get(githubAppConfiguration.getApiEndpoint(), appToken, endpoint, GithubBinding.GsonInstallation.class)
+ .map(GithubBinding.GsonInstallation::getId);
+ }
+
+ @Override
public Organizations listOrganizations(String appUrl, AccessToken accessToken, int page, int pageSize) {
checkPageArgs(page, pageSize);
diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
index c516cfafb35..855f8f66e7c 100644
--- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
+++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationClientImplTest.java
@@ -236,6 +236,39 @@ public class GithubApplicationClientImplTest {
}
@Test
+ public void getInstallationId_returns_installation_id_of_given_account() throws IOException {
+ AppToken appToken = new AppToken(APP_JWT_TOKEN);
+ when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
+ when(httpClient.get(appUrl, appToken, "/repos/torvalds/linux/installation"))
+ .thenReturn(new OkGetResponse("{" +
+ " \"id\": 2," +
+ " \"account\": {" +
+ " \"login\": \"torvalds\"" +
+ " }" +
+ "}"));
+
+ assertThat(underTest.getInstallationId(githubAppConfiguration, "torvalds/linux")).hasValue(2L);
+ }
+
+ @Test
+ public void getInstallationId_throws_IAE_if_fail_to_create_app_token() {
+ when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenThrow(IllegalArgumentException.class);
+
+ assertThatThrownBy(() -> underTest.getInstallationId(githubAppConfiguration, "torvalds"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void getInstallationId_return_empty_if_no_installation_found_for_githubAccount() throws IOException {
+ AppToken appToken = new AppToken(APP_JWT_TOKEN);
+ when(appSecurity.createAppToken(githubAppConfiguration.getId(), githubAppConfiguration.getPrivateKey())).thenReturn(appToken);
+ when(httpClient.get(appUrl, appToken, "/repos/torvalds/linux/installation"))
+ .thenReturn(new ErrorGetResponse(404, null));
+
+ assertThat(underTest.getInstallationId(githubAppConfiguration, "torvalds")).isEmpty();
+ }
+
+ @Test
@UseDataProvider("githubServers")
public void createUserAccessToken_returns_empty_if_access_token_cant_be_created(String apiUrl, String appUrl) throws IOException {
when(httpClient.post(appUrl, null, "/login/oauth/access_token?client_id=clientId&client_secret=clientSecret&code=code"))
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java
new file mode 100644
index 00000000000..514b583e734
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformService.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.almsettings.ws;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Priority;
+import org.apache.commons.lang.NotImplementedException;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+
+@ServerSide
+@Priority(1)
+public class DelegatingDevOpsPlatformService implements DevOpsPlatformService {
+
+ private final Set<DevOpsPlatformService> delegates;
+
+ public DelegatingDevOpsPlatformService(Set<DevOpsPlatformService> delegates) {
+ this.delegates = delegates;
+ }
+
+ @Override
+ public ALM getDevOpsPlatform() {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public Optional<DevOpsProjectDescriptor> getDevOpsProjectDescriptor(Map<String, String> characteristics) {
+ return delegates.stream()
+ .flatMap(delegate -> delegate.getDevOpsProjectDescriptor(characteristics).stream())
+ .findFirst();
+ }
+
+ @Override
+ public Optional<AlmSettingDto> getValidAlmSettingDto(DbSession dbSession, DevOpsProjectDescriptor devOpsProjectDescriptor) {
+ return findDelegate(devOpsProjectDescriptor.alm())
+ .flatMap(delegate -> delegate.getValidAlmSettingDto(dbSession, devOpsProjectDescriptor));
+ }
+
+ private Optional<DevOpsPlatformService> findDelegate(ALM alm) {
+ return delegates.stream()
+ .filter(delegate -> delegate.getDevOpsPlatform().equals(alm))
+ .findFirst();
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java
new file mode 100644
index 00000000000..e685e1bf9d3
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsPlatformService.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.almsettings.ws;
+
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+
+public interface DevOpsPlatformService {
+
+ ALM getDevOpsPlatform();
+
+ Optional<DevOpsProjectDescriptor> getDevOpsProjectDescriptor(Map<String, String> characteristics);
+
+ Optional<AlmSettingDto> getValidAlmSettingDto(DbSession dbSession, DevOpsProjectDescriptor devOpsProjectDescriptor);
+
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsProjectDescriptor.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsProjectDescriptor.java
new file mode 100644
index 00000000000..23db5627433
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/DevOpsProjectDescriptor.java
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.almsettings.ws;
+
+import org.sonar.db.alm.setting.ALM;
+
+public record DevOpsProjectDescriptor(ALM alm, String url, String projectIdentifier) {
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java
new file mode 100644
index 00000000000..2afec24eea7
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformService.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.almsettings.ws;
+
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.alm.client.github.GithubApplicationClient;
+import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
+import org.sonar.alm.client.github.config.GithubAppConfiguration;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDao;
+import org.sonar.db.alm.setting.AlmSettingDto;
+
+@ServerSide
+public class GitHubDevOpsPlatformService implements DevOpsPlatformService {
+
+ public static final String DEVOPS_PLATFORM_URL = "devOpsPlatformUrl";
+ public static final String DEVOPS_PLATFORM_PROJECT_IDENTIFIER = "devOpsPlatformProjectIdentifier";
+
+ private final AlmSettingDao almSettingDao;
+ private final GithubGlobalSettingsValidator githubGlobalSettingsValidator;
+ private final GithubApplicationClient githubApplicationClient;
+
+ public GitHubDevOpsPlatformService(AlmSettingDao almSettingDao, GithubGlobalSettingsValidator githubGlobalSettingsValidator,
+ GithubApplicationClient githubApplicationClient) {
+ this.almSettingDao = almSettingDao;
+ this.githubGlobalSettingsValidator = githubGlobalSettingsValidator;
+ this.githubApplicationClient = githubApplicationClient;
+ }
+
+ @Override
+ public ALM getDevOpsPlatform() {
+ return ALM.GITHUB;
+ }
+
+ @Override
+ public Optional<DevOpsProjectDescriptor> getDevOpsProjectDescriptor(Map<String, String> characteristics) {
+ String githubApiUrl = characteristics.get(DEVOPS_PLATFORM_URL);
+ String githubRepository = characteristics.get(DEVOPS_PLATFORM_PROJECT_IDENTIFIER);
+ if (githubApiUrl != null && githubRepository != null) {
+ return Optional.of(new DevOpsProjectDescriptor(ALM.GITHUB, githubApiUrl, githubRepository));
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<AlmSettingDto> getValidAlmSettingDto(DbSession dbSession, DevOpsProjectDescriptor devOpsProjectDescriptor) {
+ return almSettingDao.selectByAlm(dbSession, getDevOpsPlatform()).stream()
+ .filter(almSettingDto -> devOpsProjectDescriptor.url().equals(almSettingDto.getUrl()))
+ .filter(almSettingDto -> hasAccessToRepo(almSettingDto, devOpsProjectDescriptor.projectIdentifier()))
+ .findFirst();
+ }
+
+ private boolean hasAccessToRepo(AlmSettingDto almSettingDto, String repo) {
+ GithubAppConfiguration githubAppConfiguration = githubGlobalSettingsValidator.validate(almSettingDto);
+ return githubApplicationClient.getInstallationId(githubAppConfiguration, repo).isPresent();
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformServiceTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformServiceTest.java
new file mode 100644
index 00000000000..0a7e7460cf3
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/DelegatingDevOpsPlatformServiceTest.java
@@ -0,0 +1,109 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.almsettings.ws;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.apache.commons.lang.NotImplementedException;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDto;
+
+import static java.util.Collections.emptySet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DelegatingDevOpsPlatformServiceTest {
+
+ private static final DelegatingDevOpsPlatformService NO_DEVOPS_PLATFORMS = new DelegatingDevOpsPlatformService(emptySet());
+ private static final DelegatingDevOpsPlatformService MULTIPLE_DEVOPS_PLATFORMS = new DelegatingDevOpsPlatformService(
+ Set.of(mockGitHubDevOpsPlatformService(), mockAzureDevOpsPlatformService()));
+
+ @Mock
+ private DbSession dbSession;
+
+ @Test
+ public void getDevOpsPlatform_shouldThrow() {
+ assertThatThrownBy(NO_DEVOPS_PLATFORMS::getDevOpsPlatform)
+ .isInstanceOf(NotImplementedException.class);
+ }
+
+ @Test
+ public void getDevOpsProjectDescriptor_whenNoDelegates_shouldReturnOptionalEmpty() {
+ Optional<DevOpsProjectDescriptor> devOpsProjectDescriptor = NO_DEVOPS_PLATFORMS.getDevOpsProjectDescriptor(Map.of());
+
+ assertThat(devOpsProjectDescriptor).isEmpty();
+ }
+
+ @Test
+ public void getDevOpsProjectDescriptor_whenDelegates_shouldReturnDelegateResponse() {
+ Optional<DevOpsProjectDescriptor> devOpsProjectDescriptor = MULTIPLE_DEVOPS_PLATFORMS.getDevOpsProjectDescriptor(Map.of(
+ "githubUrl", "githubUrl"
+ ));
+
+ assertThat(devOpsProjectDescriptor)
+ .isPresent()
+ .get().usingRecursiveComparison().isEqualTo(new DevOpsProjectDescriptor(ALM.GITHUB, "githubUrl", "githubRepo"));
+ }
+
+ @Test
+ public void getValidAlmSettingDto_whenNoDelegates_shouldReturnOptionalEmpty() {
+ Optional<AlmSettingDto> almSettingDto = NO_DEVOPS_PLATFORMS.getValidAlmSettingDto(dbSession, mock(DevOpsProjectDescriptor.class));
+
+ assertThat(almSettingDto).isEmpty();
+ }
+
+ @Test
+ public void getValidAlmSettingDto_whenDelegates_shouldReturnDelegateResponse() {
+ Optional<AlmSettingDto> almSettingDto = MULTIPLE_DEVOPS_PLATFORMS.getValidAlmSettingDto(dbSession, new DevOpsProjectDescriptor(ALM.GITHUB, "githubUrl", "githubRepo"));
+
+ assertThat(almSettingDto)
+ .isPresent()
+ .get()
+ .usingRecursiveComparison().isEqualTo(new AlmSettingDto().setAlm(ALM.GITHUB));
+ }
+
+ private static DevOpsPlatformService mockGitHubDevOpsPlatformService() {
+ DevOpsPlatformService mockDevOpsPlatformService = mock();
+ when(mockDevOpsPlatformService.getDevOpsPlatform()).thenReturn(ALM.GITHUB);
+ when(mockDevOpsPlatformService.getDevOpsProjectDescriptor(Map.of("githubUrl", "githubUrl")))
+ .thenReturn(Optional.of(new DevOpsProjectDescriptor(ALM.GITHUB, "githubUrl", "githubRepo")));
+ when(mockDevOpsPlatformService.getValidAlmSettingDto(any(), any()))
+ .thenReturn(Optional.of(new AlmSettingDto().setAlm(ALM.GITHUB)));
+ return mockDevOpsPlatformService;
+ }
+
+ private static DevOpsPlatformService mockAzureDevOpsPlatformService() {
+ DevOpsPlatformService mockDevOpsPlatformService = mock();
+ when(mockDevOpsPlatformService.getDevOpsPlatform()).thenReturn(ALM.AZURE_DEVOPS);
+ when(mockDevOpsPlatformService.getDevOpsProjectDescriptor(Map.of("azureUrl", "azureUrl")))
+ .thenReturn(Optional.of(new DevOpsProjectDescriptor(ALM.AZURE_DEVOPS, "azureUrl", "azureProject")));
+ when(mockDevOpsPlatformService.getValidAlmSettingDto(any(), any()))
+ .thenReturn(Optional.of(new AlmSettingDto().setAlm(ALM.AZURE_DEVOPS)));
+ return mockDevOpsPlatformService;
+ }
+
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java
new file mode 100644
index 00000000000..2cf2a07f83f
--- /dev/null
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/almsettings/ws/GitHubDevOpsPlatformServiceTest.java
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.almsettings.ws;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+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.alm.client.github.GithubApplicationClient;
+import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
+import org.sonar.alm.client.github.config.GithubAppConfiguration;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
+import org.sonar.db.alm.setting.AlmSettingDao;
+import org.sonar.db.alm.setting.AlmSettingDto;
+
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.almsettings.ws.GitHubDevOpsPlatformService.DEVOPS_PLATFORM_PROJECT_IDENTIFIER;
+import static org.sonar.server.almsettings.ws.GitHubDevOpsPlatformService.DEVOPS_PLATFORM_URL;
+
+
+@RunWith(MockitoJUnitRunner.class)
+public class GitHubDevOpsPlatformServiceTest {
+
+ private static final DevOpsProjectDescriptor GITHUB_PROJECT_DESCRIPTOR = new DevOpsProjectDescriptor(ALM.GITHUB, "url", "repo");
+
+ @Mock
+ private DbSession dbSession;
+ @Mock
+ private AlmSettingDao almSettingDao;
+ @Mock
+ private GithubGlobalSettingsValidator githubGlobalSettingsValidator;
+ @Mock
+ private GithubApplicationClient githubApplicationClient;
+ @InjectMocks
+ private GitHubDevOpsPlatformService gitHubDevOpsPlatformService;
+
+ @Test
+ public void getDevOpsPlatform_shouldReturnGitHub() {
+ assertThat(gitHubDevOpsPlatformService.getDevOpsPlatform())
+ .isEqualTo(ALM.GITHUB);
+ }
+
+ @Test
+ public void getDevOpsProjectDescriptor_whenNoCharacteristics_shouldReturnEmpty() {
+ Optional<DevOpsProjectDescriptor> devOpsProjectDescriptor = gitHubDevOpsPlatformService.getDevOpsProjectDescriptor(Map.of());
+
+ assertThat(devOpsProjectDescriptor).isEmpty();
+ }
+
+ @Test
+ public void getDevOpsProjectDescriptor_whenValidCharacteristics_shouldReturn() {
+ Optional<DevOpsProjectDescriptor> devOpsProjectDescriptor = gitHubDevOpsPlatformService.getDevOpsProjectDescriptor(Map.of(
+ DEVOPS_PLATFORM_URL, GITHUB_PROJECT_DESCRIPTOR.url(),
+ DEVOPS_PLATFORM_PROJECT_IDENTIFIER, GITHUB_PROJECT_DESCRIPTOR.projectIdentifier()
+ ));
+
+ assertThat(devOpsProjectDescriptor)
+ .isPresent()
+ .get().usingRecursiveComparison().isEqualTo(GITHUB_PROJECT_DESCRIPTOR);
+ }
+
+ @Test
+ public void getValidAlmSettingDto_whenNoAlmSetting_shouldReturnEmpty() {
+ when(almSettingDao.selectByAlm(dbSession, ALM.GITHUB)).thenReturn(emptyList());
+
+ Optional<AlmSettingDto> almSettingDto = gitHubDevOpsPlatformService.getValidAlmSettingDto(dbSession, GITHUB_PROJECT_DESCRIPTOR);
+
+ assertThat(almSettingDto).isEmpty();
+ }
+
+ @Test
+ public void getValidAlmSettingDto_whenMultipleAlmSetting_shouldReturnTheRightOne() {
+ AlmSettingDto mockGitHubAlmSettingDtoNoAccess = mockGitHubAlmSettingDto(false);
+ AlmSettingDto mockGitHubAlmSettingDtoAccess = mockGitHubAlmSettingDto(true);
+ when(almSettingDao.selectByAlm(dbSession, ALM.GITHUB)).thenReturn(List.of(mockGitHubAlmSettingDtoNoAccess, mockGitHubAlmSettingDtoAccess));
+
+ Optional<AlmSettingDto> almSettingDto = gitHubDevOpsPlatformService.getValidAlmSettingDto(dbSession, GITHUB_PROJECT_DESCRIPTOR);
+
+ assertThat(almSettingDto)
+ .isPresent()
+ .get().isEqualTo(mockGitHubAlmSettingDtoAccess);
+ }
+
+ private AlmSettingDto mockGitHubAlmSettingDto(boolean repoAccess) {
+ AlmSettingDto mockAlmSettingDto = mock();
+ when(mockAlmSettingDto.getUrl()).thenReturn(GITHUB_PROJECT_DESCRIPTOR.url());
+ GithubAppConfiguration mockGithubAppConfiguration = mock(GithubAppConfiguration.class);
+ when(githubGlobalSettingsValidator.validate(mockAlmSettingDto)).thenReturn(mockGithubAppConfiguration);
+ when(githubApplicationClient.getInstallationId(eq(mockGithubAppConfiguration), any())).thenReturn(repoAccess ? Optional.of(1L) : Optional.empty());
+ return mockAlmSettingDto;
+ }
+
+}
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 7345e963bf0..097e0d363d5 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
@@ -62,6 +62,8 @@ import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
import org.sonar.server.almintegration.ws.github.GithubProvisioningWs;
import org.sonar.server.almsettings.MultipleAlmFeature;
import org.sonar.server.almsettings.ws.AlmSettingsWsModule;
+import org.sonar.server.almsettings.ws.DelegatingDevOpsPlatformService;
+import org.sonar.server.almsettings.ws.GitHubDevOpsPlatformService;
import org.sonar.server.authentication.AuthenticationModule;
import org.sonar.server.authentication.DefaultAdminCredentialsVerifierImpl;
import org.sonar.server.authentication.DefaultAdminCredentialsVerifierNotificationHandler;
@@ -307,6 +309,7 @@ public class PlatformLevel4 extends PlatformLevel {
DefaultBranchNameResolver.class,
DefaultDocumentationLinkGenerator.class,
DelegatingManagedServices.class,
+ DelegatingDevOpsPlatformService.class,
// batch
new BatchWsModule(),
@@ -548,6 +551,7 @@ public class PlatformLevel4 extends PlatformLevel {
GithubApplicationHttpClientImpl.class,
GithubProvisioningConfigValidator.class,
GithubProvisioningWs.class,
+ GitHubDevOpsPlatformService.class,
BitbucketCloudRestClientConfiguration.class,
BitbucketServerRestClient.class,
GitlabHttpClient.class,