3 * Copyright (C) 2009-2023 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.almintegration.ws.github;
22 import java.util.Optional;
24 import org.junit.Before;
25 import org.junit.Rule;
26 import org.junit.Test;
27 import org.mockito.ArgumentCaptor;
28 import org.sonar.alm.client.github.GithubApplicationClient;
29 import org.sonar.alm.client.github.GithubApplicationClientImpl;
30 import org.sonar.api.resources.Qualifiers;
31 import org.sonar.api.server.ws.WebService;
32 import org.sonar.api.utils.System2;
33 import org.sonar.auth.github.GitHubSettings;
34 import org.sonar.core.i18n.I18n;
35 import org.sonar.core.platform.EditionProvider;
36 import org.sonar.core.platform.PlatformEditionProvider;
37 import org.sonar.core.util.SequenceUuidFactory;
38 import org.sonar.db.DbSession;
39 import org.sonar.db.DbTester;
40 import org.sonar.db.alm.setting.AlmSettingDto;
41 import org.sonar.db.component.BranchDto;
42 import org.sonar.db.component.ResourceTypesRule;
43 import org.sonar.db.entity.EntityDto;
44 import org.sonar.db.newcodeperiod.NewCodePeriodDto;
45 import org.sonar.db.permission.GlobalPermission;
46 import org.sonar.db.project.CreationMethod;
47 import org.sonar.db.project.ProjectDto;
48 import org.sonar.db.user.UserDto;
49 import org.sonar.server.almintegration.ws.ImportHelper;
50 import org.sonar.server.almintegration.ws.ProjectKeyGenerator;
51 import org.sonar.server.component.ComponentUpdater;
52 import org.sonar.server.es.EsTester;
53 import org.sonar.server.es.IndexersImpl;
54 import org.sonar.server.es.TestIndexers;
55 import org.sonar.server.exceptions.NotFoundException;
56 import org.sonar.server.exceptions.UnauthorizedException;
57 import org.sonar.server.favorite.FavoriteUpdater;
58 import org.sonar.server.management.ManagedProjectService;
59 import org.sonar.server.newcodeperiod.NewCodeDefinitionResolver;
60 import org.sonar.server.permission.GroupPermissionChanger;
61 import org.sonar.server.permission.PermissionService;
62 import org.sonar.server.permission.PermissionServiceImpl;
63 import org.sonar.server.permission.PermissionTemplateService;
64 import org.sonar.server.permission.PermissionUpdater;
65 import org.sonar.server.permission.UserPermissionChange;
66 import org.sonar.server.permission.UserPermissionChanger;
67 import org.sonar.server.permission.index.FooIndexDefinition;
68 import org.sonar.server.permission.index.PermissionIndexer;
69 import org.sonar.server.project.DefaultBranchNameResolver;
70 import org.sonar.server.project.ProjectDefaultVisibility;
71 import org.sonar.server.project.Visibility;
72 import org.sonar.server.tester.UserSessionRule;
73 import org.sonar.server.ws.TestRequest;
74 import org.sonar.server.ws.WsActionTester;
75 import org.sonarqube.ws.Projects;
77 import static java.util.Objects.requireNonNull;
78 import static org.assertj.core.api.Assertions.assertThat;
79 import static org.assertj.core.api.Assertions.assertThatThrownBy;
80 import static org.assertj.core.api.Assertions.tuple;
81 import static org.mockito.ArgumentMatchers.any;
82 import static org.mockito.ArgumentMatchers.eq;
83 import static org.mockito.Mockito.mock;
84 import static org.mockito.Mockito.never;
85 import static org.mockito.Mockito.verify;
86 import static org.mockito.Mockito.when;
87 import static org.sonar.db.component.BranchDto.DEFAULT_MAIN_BRANCH_NAME;
88 import static org.sonar.db.newcodeperiod.NewCodePeriodType.NUMBER_OF_DAYS;
89 import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
90 import static org.sonar.server.almintegration.ws.ImportHelper.PARAM_ALM_SETTING;
91 import static org.sonar.server.almintegration.ws.github.ImportGithubProjectAction.PARAM_ORGANIZATION;
92 import static org.sonar.server.almintegration.ws.github.ImportGithubProjectAction.PARAM_REPOSITORY_KEY;
93 import static org.sonar.server.tester.UserSessionRule.standalone;
94 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_TYPE;
95 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NEW_CODE_DEFINITION_VALUE;
97 public class ImportGithubProjectActionIT {
99 private static final String PROJECT_KEY_NAME = "PROJECT_NAME";
102 public UserSessionRule userSession = standalone();
104 private final System2 system2 = mock(System2.class);
105 private final GithubApplicationClientImpl appClient = mock(GithubApplicationClientImpl.class);
106 private final DefaultBranchNameResolver defaultBranchNameResolver = mock(DefaultBranchNameResolver.class);
109 public DbTester db = DbTester.create(system2);
110 private final PermissionTemplateService permissionTemplateService = mock(PermissionTemplateService.class);
111 public EsTester es = EsTester.createCustom(new FooIndexDefinition());
112 private final PermissionUpdater<UserPermissionChange> userPermissionUpdater = new PermissionUpdater(
113 new IndexersImpl(new PermissionIndexer(db.getDbClient(), es.client())),
114 Set.of(new UserPermissionChanger(db.getDbClient(), new SequenceUuidFactory()),
115 new GroupPermissionChanger(db.getDbClient(), new SequenceUuidFactory())));
116 private final PermissionService permissionService = new PermissionServiceImpl(new ResourceTypesRule().setRootQualifiers(Qualifiers.PROJECT));
117 private final ComponentUpdater componentUpdater = new ComponentUpdater(db.getDbClient(), mock(I18n.class), System2.INSTANCE,
118 permissionTemplateService, new FavoriteUpdater(db.getDbClient()), new TestIndexers(), new SequenceUuidFactory(),
119 defaultBranchNameResolver, userPermissionUpdater, permissionService);
121 private final ImportHelper importHelper = new ImportHelper(db.getDbClient(), userSession);
122 private final ProjectKeyGenerator projectKeyGenerator = mock(ProjectKeyGenerator.class);
123 private final ProjectDefaultVisibility projectDefaultVisibility = mock(ProjectDefaultVisibility.class);
124 private PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
126 private final GitHubSettings gitHubSettings = mock(GitHubSettings.class);
127 private NewCodeDefinitionResolver newCodeDefinitionResolver = new NewCodeDefinitionResolver(db.getDbClient(), editionProvider);
129 private final ManagedProjectService managedProjectService = mock(ManagedProjectService.class);
130 private final WsActionTester ws = new WsActionTester(new ImportGithubProjectAction(db.getDbClient(), managedProjectService, userSession,
131 projectDefaultVisibility, appClient, componentUpdater, importHelper, projectKeyGenerator, newCodeDefinitionResolver,
132 defaultBranchNameResolver, gitHubSettings));
135 public void before() {
136 when(projectDefaultVisibility.get(any())).thenReturn(Visibility.PRIVATE);
137 when(defaultBranchNameResolver.getEffectiveMainBranchName()).thenReturn(DEFAULT_MAIN_BRANCH_NAME);
141 public void importProject_ifProjectWithSameNameDoesNotExist_importSucceed() {
142 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
144 GithubApplicationClient.Repository repository = mockGithubInteractions();
146 Projects.CreateWsResponse response = callWebService(githubAlmSetting);
148 Projects.CreateWsResponse.Project result = response.getProject();
149 assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
150 assertThat(result.getName()).isEqualTo(repository.getName());
152 Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
153 assertThat(projectDto).isPresent();
154 assertThat(db.getDbClient().projectAlmSettingDao().selectByProject(db.getSession(), projectDto.get())).isPresent();
155 Optional<BranchDto> mainBranch = db.getDbClient().branchDao().selectByProject(db.getSession(), projectDto.get()).stream().filter(BranchDto::isMain).findAny();
156 assertThat(mainBranch).isPresent();
157 assertThat(mainBranch.get().getKey()).isEqualTo("default-branch");
159 verify(managedProjectService).queuePermissionSyncTask(userSession.getUuid(), mainBranch.get().getUuid() , projectDto.get().getUuid());
163 public void importProject_withNCD_developer_edition() {
164 when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER));
166 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
168 mockGithubInteractions();
170 Projects.CreateWsResponse response = ws.newRequest()
171 .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
172 .setParam(PARAM_ORGANIZATION, "octocat")
173 .setParam(PARAM_REPOSITORY_KEY, "octocat/" + PROJECT_KEY_NAME)
174 .setParam(PARAM_NEW_CODE_DEFINITION_TYPE, "NUMBER_OF_DAYS")
175 .setParam(PARAM_NEW_CODE_DEFINITION_VALUE, "30")
176 .executeProtobuf(Projects.CreateWsResponse.class);
178 Projects.CreateWsResponse.Project result = response.getProject();
180 Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
181 assertThat(projectDto).isPresent();
183 assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), projectDto.get().getUuid()))
186 .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue, NewCodePeriodDto::getBranchUuid)
187 .containsExactly(NUMBER_OF_DAYS, "30", null);
191 public void importProject_withNCD_community_edition() {
192 when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.COMMUNITY));
194 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
196 mockGithubInteractions();
198 Projects.CreateWsResponse response = ws.newRequest()
199 .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
200 .setParam(PARAM_ORGANIZATION, "octocat")
201 .setParam(PARAM_REPOSITORY_KEY, "octocat/" + PROJECT_KEY_NAME)
202 .setParam(PARAM_NEW_CODE_DEFINITION_TYPE, "NUMBER_OF_DAYS")
203 .setParam(PARAM_NEW_CODE_DEFINITION_VALUE, "30")
204 .executeProtobuf(Projects.CreateWsResponse.class);
206 Projects.CreateWsResponse.Project result = response.getProject();
208 Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
209 assertThat(projectDto).isPresent();
210 BranchDto branchDto = db.getDbClient().branchDao().selectMainBranchByProjectUuid(db.getSession(), projectDto.get().getUuid()).orElseThrow();
212 String projectUuid = projectDto.get().getUuid();
213 assertThat(db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), projectUuid, branchDto.getUuid()))
216 .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue, NewCodePeriodDto::getBranchUuid)
217 .containsExactly(NUMBER_OF_DAYS, "30", branchDto.getUuid());
221 public void importProject_reference_branch_ncd_no_default_branch() {
222 when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER));
223 when(defaultBranchNameResolver.getEffectiveMainBranchName()).thenReturn("default-branch");
225 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
227 GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, PROJECT_KEY_NAME, false,
228 "octocat/" + PROJECT_KEY_NAME,
229 "https://github.sonarsource.com/api/v3/repos/octocat/" + PROJECT_KEY_NAME, null);
230 when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
231 when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
233 Projects.CreateWsResponse response = ws.newRequest()
234 .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
235 .setParam(PARAM_ORGANIZATION, "octocat")
236 .setParam(PARAM_REPOSITORY_KEY, "octocat/" + PROJECT_KEY_NAME)
237 .setParam(PARAM_NEW_CODE_DEFINITION_TYPE, "reference_branch")
238 .executeProtobuf(Projects.CreateWsResponse.class);
240 Projects.CreateWsResponse.Project result = response.getProject();
242 Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
243 assertThat(projectDto).isPresent();
245 assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), projectDto.get().getUuid()))
248 .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue)
249 .containsExactly(REFERENCE_BRANCH, "default-branch");
253 public void importProject_reference_branch_ncd() {
254 when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER));
256 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
258 GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, PROJECT_KEY_NAME, false,
259 "octocat/" + PROJECT_KEY_NAME,
260 "https://github.sonarsource.com/api/v3/repos/octocat/" + PROJECT_KEY_NAME, "mainBranch");
261 when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
262 when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
264 Projects.CreateWsResponse response = ws.newRequest()
265 .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
266 .setParam(PARAM_ORGANIZATION, "octocat")
267 .setParam(PARAM_REPOSITORY_KEY, "octocat/" + PROJECT_KEY_NAME)
268 .setParam(PARAM_NEW_CODE_DEFINITION_TYPE, "reference_branch")
269 .executeProtobuf(Projects.CreateWsResponse.class);
271 Projects.CreateWsResponse.Project result = response.getProject();
273 Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), result.getKey());
274 assertThat(projectDto).isPresent();
276 assertThat(db.getDbClient().newCodePeriodDao().selectByProject(db.getSession(), projectDto.get().getUuid()))
279 .extracting(NewCodePeriodDto::getType, NewCodePeriodDto::getValue)
280 .containsExactly(REFERENCE_BRANCH, "mainBranch");
284 public void importProject_ifProjectWithSameNameAlreadyExists_importSucceed() {
285 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
286 db.components().insertPublicProject(p -> p.setKey("Hello-World")).getMainBranchComponent();
288 GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, "Hello-World", false, "Hello-World",
289 "https://github.sonarsource.com/api/v3/repos/octocat/Hello-World", "main");
290 when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
291 when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
293 Projects.CreateWsResponse response = ws.newRequest()
294 .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
295 .setParam(PARAM_ORGANIZATION, "octocat")
296 .setParam(PARAM_REPOSITORY_KEY, "Hello-World")
297 .executeProtobuf(Projects.CreateWsResponse.class);
299 Projects.CreateWsResponse.Project result = response.getProject();
300 assertThat(result.getKey()).isEqualTo(PROJECT_KEY_NAME);
301 assertThat(result.getName()).isEqualTo(repository.getName());
305 public void importProject_whenGithubProvisioningIsDisabled_shouldApplyPermissionTemplate() {
306 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
308 mockGithubInteractions();
309 when(gitHubSettings.isProvisioningEnabled()).thenReturn(false);
312 .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
313 .setParam(PARAM_ORGANIZATION, "octocat")
314 .setParam(PARAM_REPOSITORY_KEY, "octocat/" + PROJECT_KEY_NAME)
315 .executeProtobuf(Projects.CreateWsResponse.class);
317 ArgumentCaptor<EntityDto> projectDtoArgumentCaptor = ArgumentCaptor.forClass(EntityDto.class);
318 verify(permissionTemplateService).applyDefaultToNewComponent(any(DbSession.class), projectDtoArgumentCaptor.capture(), eq(userSession.getUuid()));
319 String projectKey = projectDtoArgumentCaptor.getValue().getKey();
320 assertThat(projectKey).isEqualTo(PROJECT_KEY_NAME);
325 public void importProject_whenGithubProvisioningIsEnabled_shouldNotApplyPermissionTemplate() {
326 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
328 mockGithubInteractions();
329 when(gitHubSettings.isProvisioningEnabled()).thenReturn(true);
332 .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
333 .setParam(PARAM_ORGANIZATION, "octocat")
334 .setParam(PARAM_REPOSITORY_KEY, "octocat/" + PROJECT_KEY_NAME)
335 .executeProtobuf(Projects.CreateWsResponse.class);
337 verify(permissionTemplateService, never()).applyDefaultToNewComponent(any(), any(), any());
342 public void importProject_shouldSetCreationMethodToApi_ifNonBrowserRequest() {
343 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
344 mockGithubInteractions();
346 Projects.CreateWsResponse response = callWebService(githubAlmSetting);
348 Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), response.getProject().getKey());
349 assertThat(projectDto.orElseThrow().getCreationMethod()).isEqualTo(CreationMethod.ALM_IMPORT_API);
353 public void importProject_shouldSetCreationMethodToBrowser_ifBrowserRequest() {
354 AlmSettingDto githubAlmSetting = setupUserWithPatAndAlmSettings();
355 userSession.flagSessionAsGui();
356 mockGithubInteractions();
358 Projects.CreateWsResponse response = callWebService(githubAlmSetting);
360 Optional<ProjectDto> projectDto = db.getDbClient().projectDao().selectProjectByKey(db.getSession(), response.getProject().getKey());
361 assertThat(projectDto.orElseThrow().getCreationMethod()).isEqualTo(CreationMethod.ALM_IMPORT_BROWSER);
364 private Projects.CreateWsResponse callWebService(AlmSettingDto githubAlmSetting) {
365 return ws.newRequest()
366 .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
367 .setParam(PARAM_ORGANIZATION, "octocat")
368 .setParam(PARAM_REPOSITORY_KEY, "octocat/" + PROJECT_KEY_NAME)
369 .executeProtobuf(Projects.CreateWsResponse.class);
372 private GithubApplicationClient.Repository mockGithubInteractions() {
373 GithubApplicationClient.Repository repository = new GithubApplicationClient.Repository(1L, PROJECT_KEY_NAME, false,
374 "octocat/" + PROJECT_KEY_NAME,
375 "https://github.sonarsource.com/api/v3/repos/octocat/" + PROJECT_KEY_NAME, "default-branch");
376 when(appClient.getRepository(any(), any(), any(), any())).thenReturn(Optional.of(repository));
377 when(projectKeyGenerator.generateUniqueProjectKey(repository.getFullName())).thenReturn(PROJECT_KEY_NAME);
382 public void fail_when_not_logged_in() {
383 TestRequest request = ws.newRequest()
384 .setParam(PARAM_ALM_SETTING, "asdfghjkl")
385 .setParam(PARAM_ORGANIZATION, "test")
386 .setParam(PARAM_REPOSITORY_KEY, "test/repo");
387 assertThatThrownBy(request::execute)
388 .isInstanceOf(UnauthorizedException.class);
392 public void fail_when_missing_create_project_permission() {
393 TestRequest request = ws.newRequest();
394 assertThatThrownBy(request::execute)
395 .isInstanceOf(UnauthorizedException.class);
399 public void fail_when_almSetting_does_not_exist() {
400 UserDto user = db.users().insertUser();
401 userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS);
403 TestRequest request = ws.newRequest()
404 .setParam(PARAM_ALM_SETTING, "unknown")
405 .setParam(PARAM_ORGANIZATION, "test")
406 .setParam(PARAM_REPOSITORY_KEY, "test/repo");
407 assertThatThrownBy(request::execute)
408 .isInstanceOf(NotFoundException.class)
409 .hasMessage("DevOps Platform Setting 'unknown' not found");
413 public void fail_when_personal_access_token_doesnt_exist() {
414 AlmSettingDto githubAlmSetting = setupUserAndAlmSettings();
416 TestRequest request = ws.newRequest()
417 .setParam(PARAM_ALM_SETTING, githubAlmSetting.getKey())
418 .setParam(PARAM_ORGANIZATION, "test")
419 .setParam(PARAM_REPOSITORY_KEY, "test/repo");
420 assertThatThrownBy(request::execute)
421 .isInstanceOf(IllegalArgumentException.class)
422 .hasMessage("No personal access token found");
426 public void definition() {
427 WebService.Action def = ws.getDef();
429 assertThat(def.since()).isEqualTo("8.4");
430 assertThat(def.isPost()).isTrue();
431 assertThat(def.params())
432 .extracting(WebService.Param::key, WebService.Param::isRequired)
433 .containsExactlyInAnyOrder(
434 tuple(PARAM_ALM_SETTING, true),
435 tuple(PARAM_ORGANIZATION, true),
436 tuple(PARAM_REPOSITORY_KEY, true),
437 tuple(PARAM_NEW_CODE_DEFINITION_TYPE, false),
438 tuple(PARAM_NEW_CODE_DEFINITION_VALUE, false));
441 private AlmSettingDto setupUserWithPatAndAlmSettings() {
442 AlmSettingDto almSettings = setupUserAndAlmSettings();
443 db.almPats().insert(p -> p.setAlmSettingUuid(almSettings.getUuid()).setUserUuid(requireNonNull(userSession.getUuid())));
447 private AlmSettingDto setupUserAndAlmSettings() {
448 UserDto user = db.users().insertUser();
449 userSession.logIn(user).addPermission(GlobalPermission.PROVISION_PROJECTS);
450 return db.almSettings().insertGitHubAlmSetting(alm -> alm.setClientId("client_123").setClientSecret("client_secret_123"));