3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.common.almsettings.github;
22 import java.util.List;
24 import java.util.Optional;
25 import org.junit.Test;
26 import org.junit.runner.RunWith;
27 import org.mockito.Answers;
28 import org.mockito.InjectMocks;
29 import org.mockito.Mock;
30 import org.mockito.junit.MockitoJUnitRunner;
31 import org.sonar.alm.client.github.GithubGlobalSettingsValidator;
32 import org.sonar.alm.client.github.GithubPermissionConverter;
33 import org.sonar.auth.github.AppInstallationToken;
34 import org.sonar.auth.github.GitHubSettings;
35 import org.sonar.auth.github.client.GithubApplicationClient;
36 import org.sonar.auth.github.security.AccessToken;
37 import org.sonar.auth.github.security.UserAccessToken;
38 import org.sonar.db.DbClient;
39 import org.sonar.db.DbSession;
40 import org.sonar.db.alm.pat.AlmPatDto;
41 import org.sonar.db.alm.setting.ALM;
42 import org.sonar.db.alm.setting.AlmSettingDto;
43 import org.sonar.server.common.almintegration.ProjectKeyGenerator;
44 import org.sonar.server.common.almsettings.DevOpsProjectCreationContext;
45 import org.sonar.server.common.almsettings.DevOpsProjectCreator;
46 import org.sonar.server.common.almsettings.DevOpsProjectDescriptor;
47 import org.sonar.server.common.permission.PermissionUpdater;
48 import org.sonar.server.common.permission.UserPermissionChange;
49 import org.sonar.server.common.project.ProjectCreator;
50 import org.sonar.server.exceptions.BadConfigurationException;
51 import org.sonar.server.management.ManagedProjectService;
52 import org.sonar.server.permission.PermissionService;
53 import org.sonar.server.project.ProjectDefaultVisibility;
55 import static java.lang.String.format;
56 import static org.assertj.core.api.Assertions.assertThat;
57 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
58 import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
59 import static org.assertj.core.api.Assertions.assertThatThrownBy;
60 import static org.mockito.ArgumentMatchers.any;
61 import static org.mockito.ArgumentMatchers.eq;
62 import static org.mockito.Mockito.mock;
63 import static org.mockito.Mockito.when;
64 import static org.sonar.core.ce.CeTaskCharacteristics.DEVOPS_PLATFORM_PROJECT_IDENTIFIER;
65 import static org.sonar.core.ce.CeTaskCharacteristics.DEVOPS_PLATFORM_URL;
67 @RunWith(MockitoJUnitRunner.class)
68 public class GithubProjectCreatorFactoryTest {
69 private static final String PROJECT_NAME = "projectName";
70 private static final String ORGANIZATION_NAME = "orgname";
71 private static final String GITHUB_REPO_FULL_NAME = ORGANIZATION_NAME + "/" + PROJECT_NAME;
72 private static final String GITHUB_API_URL = "https://api.toto.com";
74 private static final DevOpsProjectDescriptor GITHUB_PROJECT_DESCRIPTOR = new DevOpsProjectDescriptor(ALM.GITHUB, GITHUB_API_URL, GITHUB_REPO_FULL_NAME, null);
75 private static final Map<String, String> VALID_GITHUB_PROJECT_COORDINATES = Map.of(
76 DEVOPS_PLATFORM_URL, GITHUB_PROJECT_DESCRIPTOR.url(),
77 DEVOPS_PLATFORM_PROJECT_IDENTIFIER, GITHUB_PROJECT_DESCRIPTOR.repositoryIdentifier());
78 private static final long APP_INSTALLATION_ID = 534534534543L;
79 private static final String USER_ACCESS_TOKEN = "userPat";
80 private static final DevOpsProjectCreationContext DEV_OPS_PROJECT = mock();
83 private DbSession dbSession;
85 private GithubGlobalSettingsValidator githubGlobalSettingsValidator;
87 private GithubApplicationClient githubApplicationClient;
88 @Mock(answer = Answers.RETURNS_DEEP_STUBS)
89 private DbClient dbClient;
90 @Mock(answer = Answers.RETURNS_DEEP_STUBS)
91 private ProjectDefaultVisibility projectDefaultVisibility;
93 private ProjectKeyGenerator projectKeyGenerator;
95 private GitHubSettings gitHubSettings;
97 private GithubPermissionConverter githubPermissionConverter;
99 private AppInstallationToken appInstallationToken;
101 private AppInstallationToken authAppInstallationToken;
103 private PermissionService permissionService;
105 private PermissionUpdater<UserPermissionChange> permissionUpdater;
107 private ManagedProjectService managedProjectService;
109 private ProjectCreator projectCreator;
111 private GithubDevOpsProjectCreationContextService devOpsProjectService;
114 private GithubProjectCreatorFactory githubProjectCreatorFactory;
117 public void getDevOpsProjectCreator_whenNoCharacteristics_shouldReturnEmpty() {
118 Optional<DevOpsProjectCreator> devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, Map.of());
120 assertThat(devOpsProjectCreator).isEmpty();
124 public void getDevOpsProjectCreator_whenValidCharacteristicsButNoAlmSettingDao_shouldReturnEmpty() {
125 Optional<DevOpsProjectCreator> devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES);
126 assertThat(devOpsProjectCreator).isEmpty();
130 public void getDevOpsProjectCreator_whenValidCharacteristicsButInvalidAlmSettingDto_shouldThrow() {
131 AlmSettingDto almSettingDto = mockAlmSettingDto(true);
132 IllegalArgumentException error = new IllegalArgumentException("error happened");
133 when(githubGlobalSettingsValidator.validate(almSettingDto)).thenThrow(error);
135 assertThatIllegalArgumentException().isThrownBy(() -> githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES))
140 public void getDevOpsProjectCreator_whenAppHasNoAccessToRepo_shouldReturnEmpty() {
141 mockAlmSettingDto(true);
142 when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.empty());
144 Optional<DevOpsProjectCreator> devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES);
145 assertThat(devOpsProjectCreator).isEmpty();
149 public void getDevOpsProjectCreator_whenNotPossibleToGenerateToken_shouldThrow() {
150 AlmSettingDto almSettingDto = mockAlmSettingDto(true);
151 when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.of(APP_INSTALLATION_ID));
152 when(githubGlobalSettingsValidator.validate(almSettingDto)).thenReturn(mock());
153 when(githubApplicationClient.createAppInstallationToken(any(), eq(APP_INSTALLATION_ID))).thenReturn(Optional.empty());
155 assertThatIllegalStateException().isThrownBy(() -> githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES))
156 .withMessage("Error while generating token for GitHub Api Url null (installation id: 534534534543)");
160 public void getDevOpsProjectCreator_whenOneValidAlmSetting_shouldInstantiateDevOpsProjectCreator() {
161 AlmSettingDto almSettingDto = mockAlmSettingDto(true);
162 mockSuccessfulGithubInteraction();
164 when(devOpsProjectService.createDevOpsProject(almSettingDto, GITHUB_PROJECT_DESCRIPTOR, appInstallationToken)).thenReturn(DEV_OPS_PROJECT);
165 DevOpsProjectCreator devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES).orElseThrow();
167 GithubProjectCreator expectedGithubProjectCreator = getExpectedGithubProjectCreator(almSettingDto, false, appInstallationToken);
168 assertThat(devOpsProjectCreator).usingRecursiveComparison().isEqualTo(expectedGithubProjectCreator);
172 public void getDevOpsProjectCreator_whenOneValidAlmSettingAndPublicByDefaultAndAutoProvisioningEnabled_shouldInstantiateDevOpsProjectCreatorAndDefineAnAuthAppToken() {
173 AlmSettingDto almSettingDto = mockAlmSettingDto(true);
174 mockSuccessfulGithubInteraction();
176 when(projectDefaultVisibility.get(any()).isPrivate()).thenReturn(true);
177 mockValidGitHubSettings();
179 long authAppInstallationId = 32;
180 when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.of(authAppInstallationId));
181 when(githubApplicationClient.createAppInstallationToken(any(), eq(authAppInstallationId))).thenReturn(Optional.of(authAppInstallationToken));
182 when(DEV_OPS_PROJECT.devOpsPlatformIdentifier()).thenReturn(GITHUB_REPO_FULL_NAME);
184 when(devOpsProjectService.createDevOpsProject(almSettingDto, GITHUB_PROJECT_DESCRIPTOR, authAppInstallationToken)).thenReturn(DEV_OPS_PROJECT);
185 DevOpsProjectCreator devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES).orElseThrow();
187 GithubProjectCreator expectedGithubProjectCreator = getExpectedGithubProjectCreator(almSettingDto, true, appInstallationToken);
188 assertThat(devOpsProjectCreator).usingRecursiveComparison().isEqualTo(expectedGithubProjectCreator);
192 public void getDevOpsProjectCreator_whenOneMatchingAndOneNotMatchingAlmSetting_shouldInstantiateDevOpsProjectCreator() {
193 AlmSettingDto matchingAlmSettingDto = mockAlmSettingDto(true);
194 AlmSettingDto notMatchingAlmSettingDto = mockAlmSettingDto(false);
195 when(dbClient.almSettingDao().selectByAlm(dbSession, ALM.GITHUB)).thenReturn(List.of(notMatchingAlmSettingDto, matchingAlmSettingDto));
197 mockSuccessfulGithubInteraction();
199 when(devOpsProjectService.createDevOpsProject(matchingAlmSettingDto, GITHUB_PROJECT_DESCRIPTOR, appInstallationToken)).thenReturn(DEV_OPS_PROJECT);
200 DevOpsProjectCreator devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(dbSession, VALID_GITHUB_PROJECT_COORDINATES).orElseThrow();
202 GithubProjectCreator expectedGithubProjectCreator = getExpectedGithubProjectCreator(matchingAlmSettingDto, false, appInstallationToken);
203 assertThat(devOpsProjectCreator).usingRecursiveComparison().isEqualTo(expectedGithubProjectCreator);
207 public void getDevOpsProjectCreatorFromImport_shouldInstantiateDevOpsProjectCreator() {
208 AlmSettingDto mockAlmSettingDto = mockAlmSettingDto(true);
209 mockAlmPatDto(mockAlmSettingDto);
211 mockSuccessfulGithubInteraction();
213 when(devOpsProjectService.create(mockAlmSettingDto, GITHUB_PROJECT_DESCRIPTOR)).thenReturn(DEV_OPS_PROJECT);
214 DevOpsProjectCreator devOpsProjectCreator = githubProjectCreatorFactory.getDevOpsProjectCreator(mockAlmSettingDto, GITHUB_PROJECT_DESCRIPTOR).orElseThrow();
216 GithubProjectCreator expectedGithubProjectCreator = getExpectedGithubProjectCreator(mockAlmSettingDto, false, new UserAccessToken(USER_ACCESS_TOKEN));
217 assertThat(devOpsProjectCreator).usingRecursiveComparison().isEqualTo(expectedGithubProjectCreator);
221 public void getDevOpsProjectCreatorFromImport_whenGitHubConfigDoesNotAllowAccessToRepo_shouldThrow() {
222 AlmSettingDto mockAlmSettingDto = mockAlmSettingDto(false);
223 mockAlmPatDto(mockAlmSettingDto);
225 mockValidGitHubSettings();
227 when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.empty());
228 when(devOpsProjectService.create(mockAlmSettingDto, GITHUB_PROJECT_DESCRIPTOR)).thenReturn(DEV_OPS_PROJECT);
229 when(DEV_OPS_PROJECT.devOpsPlatformIdentifier()).thenReturn(GITHUB_PROJECT_DESCRIPTOR.repositoryIdentifier());
231 assertThatThrownBy(() -> githubProjectCreatorFactory.getDevOpsProjectCreator(mockAlmSettingDto, GITHUB_PROJECT_DESCRIPTOR))
232 .isInstanceOf(BadConfigurationException.class)
233 .hasMessage(format("GitHub auto-provisioning is activated. However the repo %s is not in the scope of the authentication application. "
234 + "The permissions can't be checked, and the project can not be created.",
235 GITHUB_REPO_FULL_NAME));
238 private void mockValidGitHubSettings() {
239 when(gitHubSettings.appId()).thenReturn("4324");
240 when(gitHubSettings.privateKey()).thenReturn("privateKey");
241 when(gitHubSettings.apiURL()).thenReturn(GITHUB_API_URL);
242 when(gitHubSettings.isProvisioningEnabled()).thenReturn(true);
245 private void mockSuccessfulGithubInteraction() {
246 when(githubApplicationClient.getInstallationId(any(), eq(GITHUB_REPO_FULL_NAME))).thenReturn(Optional.of(APP_INSTALLATION_ID));
247 when(githubApplicationClient.createAppInstallationToken(any(), eq(APP_INSTALLATION_ID))).thenReturn(Optional.of(appInstallationToken));
250 private GithubProjectCreator getExpectedGithubProjectCreator(AlmSettingDto almSettingDto, boolean isInstanceManaged, AccessToken accessToken) {
251 AppInstallationToken authAppInstallToken = isInstanceManaged ? authAppInstallationToken : null;
252 return new GithubProjectCreator(dbClient, DEV_OPS_PROJECT,
253 projectKeyGenerator, gitHubSettings, projectCreator, permissionService, permissionUpdater, managedProjectService, githubApplicationClient,
254 githubPermissionConverter, authAppInstallToken);
257 private AlmSettingDto mockAlmSettingDto(boolean repoAccess) {
258 AlmSettingDto almSettingDto = mock();
259 when(almSettingDto.getUrl()).thenReturn(repoAccess ? GITHUB_PROJECT_DESCRIPTOR.url() : "anotherUrl");
260 when(almSettingDto.getAlm()).thenReturn(ALM.GITHUB);
262 when(dbClient.almSettingDao().selectByAlm(dbSession, ALM.GITHUB)).thenReturn(List.of(almSettingDto));
263 return almSettingDto;
266 private void mockAlmPatDto(AlmSettingDto almSettingDto) {
267 when(dbClient.almPatDao().selectByUserAndAlmSetting(any(), eq("userUuid"), eq(almSettingDto)))
268 .thenReturn(Optional.of(new AlmPatDto().setPersonalAccessToken(USER_ACCESS_TOKEN)));