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.almintegration.ws.azure;
22 import com.google.common.collect.ImmutableList;
23 import org.jetbrains.annotations.NotNull;
24 import org.junit.Before;
25 import org.junit.Rule;
26 import org.junit.Test;
27 import org.sonar.alm.client.azure.AzureDevOpsHttpClient;
28 import org.sonar.alm.client.azure.GsonAzureProject;
29 import org.sonar.alm.client.azure.GsonAzureRepo;
30 import org.sonar.alm.client.azure.GsonAzureRepoList;
31 import org.sonar.api.config.internal.Encryption;
32 import org.sonar.api.server.ws.WebService;
33 import org.sonar.db.DbTester;
34 import org.sonar.db.alm.pat.AlmPatDto;
35 import org.sonar.db.alm.setting.AlmSettingDto;
36 import org.sonar.db.project.ProjectDto;
37 import org.sonar.db.user.UserDto;
38 import org.sonar.server.exceptions.ForbiddenException;
39 import org.sonar.server.exceptions.NotFoundException;
40 import org.sonar.server.exceptions.UnauthorizedException;
41 import org.sonar.server.tester.UserSessionRule;
42 import org.sonar.server.ws.TestRequest;
43 import org.sonar.server.ws.WsActionTester;
45 import static java.util.Collections.emptyList;
46 import static org.assertj.core.api.Assertions.assertThat;
47 import static org.assertj.core.api.Assertions.assertThatThrownBy;
48 import static org.assertj.core.api.Assertions.tuple;
49 import static org.mockito.ArgumentMatchers.any;
50 import static org.mockito.Mockito.mock;
51 import static org.mockito.Mockito.when;
52 import static org.sonar.db.alm.integration.pat.AlmPatsTesting.newAlmPatDto;
53 import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS;
54 import static org.sonarqube.ws.AlmIntegrations.AzureRepo;
55 import static org.sonarqube.ws.AlmIntegrations.SearchAzureReposWsResponse;
57 public class SearchAzureReposActionIT {
60 public UserSessionRule userSession = UserSessionRule.standalone();
62 public DbTester db = DbTester.create();
64 private final AzureDevOpsHttpClient azureDevOpsHttpClient = mock(AzureDevOpsHttpClient.class);
65 private final Encryption encryption = mock(Encryption.class);
66 private final WsActionTester ws = new WsActionTester(new SearchAzureReposAction(db.getDbClient(), userSession, azureDevOpsHttpClient));
69 public void before() {
70 mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("project-1", "repoName-1"),
71 getGsonAzureRepo("project-2", "repoName-2"))));
75 public void define() {
76 WebService.Action def = ws.getDef();
78 assertThat(def.since()).isEqualTo("8.6");
79 assertThat(def.isPost()).isFalse();
80 assertThat(def.responseExampleFormat()).isEqualTo("json");
81 assertThat(def.params())
82 .extracting(WebService.Param::key, WebService.Param::isRequired)
83 .containsExactlyInAnyOrder(
84 tuple("almSetting", true),
85 tuple("projectName", false),
86 tuple("searchQuery", false));
90 public void search_repos() {
91 AlmSettingDto almSetting = insertAlmSetting();
93 SearchAzureReposWsResponse response = ws.newRequest()
94 .setParam("almSetting", almSetting.getKey())
95 .executeProtobuf(SearchAzureReposWsResponse.class);
97 assertThat(response.getRepositoriesList())
98 .extracting(AzureRepo::getName, AzureRepo::getProjectName)
99 .containsExactlyInAnyOrder(
100 tuple("repoName-1", "project-1"), tuple("repoName-2", "project-2"));
104 public void search_repos_alphabetically_sorted() {
105 mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("project-1", "Z-repo"),
106 getGsonAzureRepo("project-1", "A-repo-1"), getGsonAzureRepo("project-1", "a-repo"),
107 getGsonAzureRepo("project-1", "b-repo"))));
109 AlmSettingDto almSetting = insertAlmSetting();
111 SearchAzureReposWsResponse response = ws.newRequest()
112 .setParam("almSetting", almSetting.getKey())
113 .executeProtobuf(SearchAzureReposWsResponse.class);
115 assertThat(response.getRepositoriesList())
116 .extracting(AzureRepo::getName, AzureRepo::getProjectName)
118 tuple("a-repo", "project-1"), tuple("A-repo-1", "project-1"),
119 tuple("b-repo", "project-1"), tuple("Z-repo", "project-1"));
123 public void search_repos_with_project_already_set_up() {
124 AlmSettingDto almSetting = insertAlmSetting();
126 ProjectDto projectDto2 = insertProject(almSetting, "repoName-2", "project-2");
128 SearchAzureReposWsResponse response = ws.newRequest()
129 .setParam("almSetting", almSetting.getKey())
130 .executeProtobuf(SearchAzureReposWsResponse.class);
132 assertThat(response.getRepositoriesCount()).isEqualTo(2);
134 assertThat(response.getRepositoriesList())
135 .extracting(AzureRepo::getName, AzureRepo::getProjectName,
136 AzureRepo::getSqProjectKey, AzureRepo::getSqProjectName)
137 .containsExactlyInAnyOrder(
138 tuple("repoName-1", "project-1", "", ""),
139 tuple("repoName-2", "project-2", projectDto2.getKey(), projectDto2.getName()));
143 public void search_repos_with_project_already_set_u_and_collision_is_handled() {
144 AlmSettingDto almSetting = insertAlmSetting();
146 ProjectDto projectDto2 = insertProject(almSetting, "repoName-2", "project-2");
147 insertProject(almSetting, "repoName-2", "project-2");
149 SearchAzureReposWsResponse response = ws.newRequest()
150 .setParam("almSetting", almSetting.getKey())
151 .executeProtobuf(SearchAzureReposWsResponse.class);
153 assertThat(response.getRepositoriesCount()).isEqualTo(2);
155 assertThat(response.getRepositoriesList())
156 .extracting(AzureRepo::getName, AzureRepo::getProjectName,
157 AzureRepo::getSqProjectKey, AzureRepo::getSqProjectName)
158 .containsExactlyInAnyOrder(
159 tuple("repoName-1", "project-1", "", ""),
160 tuple("repoName-2", "project-2", projectDto2.getKey(), projectDto2.getName()));
164 public void search_repos_with_projects_already_set_up_and_no_collision() {
165 mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("project-1", "repoName-1"),
166 getGsonAzureRepo("project", "1-repoName-1"))));
167 AlmSettingDto almSetting = insertAlmSetting();
169 ProjectDto projectDto1 = insertProject(almSetting, "repoName-1", "project-1");
170 ProjectDto projectDto2 = insertProject(almSetting, "1-repoName-1", "project");
172 SearchAzureReposWsResponse response = ws.newRequest()
173 .setParam("almSetting", almSetting.getKey())
174 .executeProtobuf(SearchAzureReposWsResponse.class);
176 assertThat(response.getRepositoriesCount()).isEqualTo(2);
178 assertThat(response.getRepositoriesList())
179 .extracting(AzureRepo::getName, AzureRepo::getProjectName,
180 AzureRepo::getSqProjectKey, AzureRepo::getSqProjectName)
181 .containsExactlyInAnyOrder(
182 tuple("repoName-1", "project-1", projectDto1.getKey(), projectDto1.getName()),
183 tuple("1-repoName-1", "project", projectDto2.getKey(), projectDto2.getName()));
187 public void search_repos_with_same_name_and_different_project() {
188 mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("project-1", "repoName-1"),
189 getGsonAzureRepo("project-2", "repoName-1"))));
190 AlmSettingDto almSetting = insertAlmSetting();
192 ProjectDto projectDto1 = insertProject(almSetting, "repoName-1", "project-1");
193 ProjectDto projectDto2 = insertProject(almSetting, "repoName-1", "project-2");
195 SearchAzureReposWsResponse response = ws.newRequest()
196 .setParam("almSetting", almSetting.getKey())
197 .executeProtobuf(SearchAzureReposWsResponse.class);
199 assertThat(response.getRepositoriesCount()).isEqualTo(2);
201 assertThat(response.getRepositoriesList())
202 .extracting(AzureRepo::getName, AzureRepo::getProjectName,
203 AzureRepo::getSqProjectKey, AzureRepo::getSqProjectName)
204 .containsExactlyInAnyOrder(
205 tuple("repoName-1", "project-1", projectDto1.getKey(), projectDto1.getName()),
206 tuple("repoName-1", "project-2", projectDto2.getKey(), projectDto2.getName()));
210 public void search_repos_with_project_name() {
211 AlmSettingDto almSetting = insertAlmSetting();
213 SearchAzureReposWsResponse response = ws.newRequest()
214 .setParam("almSetting", almSetting.getKey())
215 .setParam("projectName", "project-1")
216 .executeProtobuf(SearchAzureReposWsResponse.class);
218 assertThat(response.getRepositoriesList())
219 .extracting(AzureRepo::getName, AzureRepo::getProjectName)
220 .containsExactlyInAnyOrder(
221 tuple("repoName-1", "project-1"), tuple("repoName-2", "project-2"));
225 public void search_repos_with_project_name_and_empty_criteria() {
226 AlmSettingDto almSetting = insertAlmSetting();
228 SearchAzureReposWsResponse response = ws.newRequest()
229 .setParam("almSetting", almSetting.getKey())
230 .setParam("projectName", "project-1")
231 .setParam("searchQuery", "")
232 .executeProtobuf(SearchAzureReposWsResponse.class);
234 assertThat(response.getRepositoriesList())
235 .extracting(AzureRepo::getName, AzureRepo::getProjectName)
236 .containsExactlyInAnyOrder(
237 tuple("repoName-1", "project-1"), tuple("repoName-2", "project-2"));
241 public void search_and_filter_repos_with_repo_name() {
242 AlmSettingDto almSetting = insertAlmSetting();
244 SearchAzureReposWsResponse response = ws.newRequest()
245 .setParam("almSetting", almSetting.getKey())
246 .setParam("searchQuery", "repoName-2")
247 .executeProtobuf(SearchAzureReposWsResponse.class);
249 assertThat(response.getRepositoriesList())
250 .extracting(AzureRepo::getName, AzureRepo::getProjectName)
251 .containsExactlyInAnyOrder(tuple("repoName-2", "project-2"));
255 public void search_and_filter_repos_with_matching_repo_and_project_name() {
256 mockClient(new GsonAzureRepoList(ImmutableList.of(getGsonAzureRepo("big-project", "repo-1"),
257 getGsonAzureRepo("big-project", "repo-2"),
258 getGsonAzureRepo("big-project", "big-repo"),
259 getGsonAzureRepo("project", "big-repo"),
260 getGsonAzureRepo("project", "small-repo"))));
261 AlmSettingDto almSetting = insertAlmSetting();
263 SearchAzureReposWsResponse response = ws.newRequest()
264 .setParam("almSetting", almSetting.getKey())
265 .setParam("searchQuery", "big")
266 .executeProtobuf(SearchAzureReposWsResponse.class);
268 assertThat(response.getRepositoriesList())
269 .extracting(AzureRepo::getName, AzureRepo::getProjectName)
270 .containsExactlyInAnyOrder(tuple("repo-1", "big-project"), tuple("repo-2", "big-project"),
271 tuple("big-repo", "big-project"), tuple("big-repo", "project"));
275 public void return_empty_list_when_there_are_no_azure_repos() {
276 when(azureDevOpsHttpClient.getRepos(any(), any(), any())).thenReturn(new GsonAzureRepoList(emptyList()));
278 AlmSettingDto almSetting = insertAlmSetting();
280 SearchAzureReposWsResponse response = ws.newRequest()
281 .setParam("almSetting", almSetting.getKey())
282 .executeProtobuf(SearchAzureReposWsResponse.class);
284 assertThat(response.getRepositoriesList()).isEmpty();
288 public void check_pat_is_missing() {
290 AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting();
292 TestRequest request = ws.newRequest()
293 .setParam("almSetting", almSetting.getKey());
295 assertThatThrownBy(request::execute)
296 .isInstanceOf(IllegalArgumentException.class)
297 .hasMessage("No personal access token found");
301 public void fail_check_pat_alm_setting_not_found() {
302 UserDto user = insertUser();
303 AlmPatDto almPatDto = newAlmPatDto();
304 db.getDbClient().almPatDao().insert(db.getSession(), almPatDto, user.getLogin(), null);
306 TestRequest request = ws.newRequest()
307 .setParam("almSetting", "testKey");
309 assertThatThrownBy(request::execute)
310 .isInstanceOf(NotFoundException.class)
311 .hasMessage("DevOps Platform Setting 'testKey' not found");
315 public void fail_when_not_logged_in() {
316 TestRequest request = ws.newRequest()
317 .setParam("almSetting", "anyvalue");
319 assertThatThrownBy(request::execute)
320 .isInstanceOf(UnauthorizedException.class);
324 public void fail_when_no_creation_project_permission() {
325 UserDto user = db.users().insertUser();
326 userSession.logIn(user);
328 TestRequest request = ws.newRequest()
329 .setParam("almSetting", "anyvalue");
331 assertThatThrownBy(request::execute)
332 .isInstanceOf(ForbiddenException.class)
333 .hasMessage("Insufficient privileges");
336 private ProjectDto insertProject(AlmSettingDto almSetting, String repoName, String projectName) {
337 ProjectDto projectDto1 = db.components().insertPrivateProject().getProjectDto();
338 db.almSettings().insertAzureProjectAlmSetting(almSetting, projectDto1, projectAlmSettingDto -> projectAlmSettingDto.setAlmRepo(repoName),
339 projectAlmSettingDto -> projectAlmSettingDto.setAlmSlug(projectName));
343 private void mockClient(GsonAzureRepoList repoList) {
344 when(azureDevOpsHttpClient.getRepos(any(), any(), any())).thenReturn(repoList);
347 private AlmSettingDto insertAlmSetting() {
348 UserDto user = insertUser();
349 AlmSettingDto almSetting = db.almSettings().insertAzureAlmSetting();
350 db.almPats().insert(dto -> {
351 dto.setAlmSettingUuid(almSetting.getUuid());
352 dto.setUserUuid(user.getUuid());
353 dto.setPersonalAccessToken(almSetting.getDecryptedPersonalAccessToken(encryption));
359 private UserDto insertUser() {
360 UserDto user = db.users().insertUser();
361 userSession.logIn(user).addPermission(PROVISION_PROJECTS);
365 private GsonAzureRepo getGsonAzureRepo(String projectName, String repoName) {
366 GsonAzureProject project = new GsonAzureProject(projectName, "the best project ever");
367 return new GsonAzureRepo("repo-id", repoName, "url", project, "repo-default-branch");