You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SearchActionIT.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  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.
  10. *
  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.
  15. *
  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.
  19. */
  20. package org.sonar.server.project.ws;
  21. import com.google.common.base.Joiner;
  22. import java.time.OffsetDateTime;
  23. import java.time.ZoneOffset;
  24. import java.util.Arrays;
  25. import java.util.Collections;
  26. import java.util.Date;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Set;
  30. import org.junit.Rule;
  31. import org.junit.Test;
  32. import org.sonar.api.server.ws.WebService;
  33. import org.sonar.api.server.ws.WebService.Param;
  34. import org.sonar.api.utils.DateUtils;
  35. import org.sonar.db.DbTester;
  36. import org.sonar.db.component.BranchDto;
  37. import org.sonar.db.component.ComponentDto;
  38. import org.sonar.db.component.ProjectData;
  39. import org.sonar.db.project.ProjectDto;
  40. import org.sonar.server.exceptions.ForbiddenException;
  41. import org.sonar.server.management.ManagedProjectService;
  42. import org.sonar.server.tester.UserSessionRule;
  43. import org.sonar.server.ws.TestRequest;
  44. import org.sonar.server.ws.WsActionTester;
  45. import org.sonarqube.ws.MediaTypes;
  46. import org.sonarqube.ws.Projects.SearchWsResponse;
  47. import org.sonarqube.ws.Projects.SearchWsResponse.Component;
  48. import org.sonarqube.ws.client.component.ComponentsWsParameters;
  49. import static java.util.Arrays.asList;
  50. import static java.util.Collections.singletonList;
  51. import static java.util.Optional.ofNullable;
  52. import static org.assertj.core.api.Assertions.assertThat;
  53. import static org.assertj.core.api.Assertions.assertThatThrownBy;
  54. import static org.assertj.core.api.Assertions.tuple;
  55. import static org.mockito.ArgumentMatchers.any;
  56. import static org.mockito.Mockito.eq;
  57. import static org.mockito.Mockito.mock;
  58. import static org.mockito.Mockito.when;
  59. import static org.sonar.api.server.ws.WebService.Param.PAGE;
  60. import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
  61. import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
  62. import static org.sonar.api.utils.DateUtils.formatDateTime;
  63. import static org.sonar.api.utils.DateUtils.parseDateTime;
  64. import static org.sonar.db.component.ComponentTesting.newDirectory;
  65. import static org.sonar.db.component.ComponentTesting.newFileDto;
  66. import static org.sonar.db.component.SnapshotTesting.newAnalysis;
  67. import static org.sonar.db.permission.GlobalPermission.ADMINISTER;
  68. import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
  69. import static org.sonar.test.JsonAssert.assertJson;
  70. import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE;
  71. import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ON_PROVISIONED_ONLY;
  72. import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECTS;
  73. import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY;
  74. public class SearchActionIT {
  75. private static final String PROJECT_KEY_1 = "project1";
  76. private static final String PROJECT_KEY_2 = "project2";
  77. private static final String PROJECT_KEY_3 = "project3";
  78. @Rule
  79. public final UserSessionRule userSession = UserSessionRule.standalone();
  80. @Rule
  81. public final DbTester db = DbTester.create();
  82. private final ManagedProjectService managedProjectService = mock(ManagedProjectService.class);
  83. private final WsActionTester ws = new WsActionTester(new SearchAction(db.getDbClient(), userSession, managedProjectService));
  84. @Test
  85. public void search_by_key_query_with_partial_match_case_insensitive() {
  86. userSession.addPermission(ADMINISTER);
  87. db.components().insertPrivateProject(p -> p.setKey("project-_%-key")).getMainBranchComponent();
  88. db.components().insertPrivateProject(p -> p.setKey("PROJECT-_%-KEY")).getMainBranchComponent();
  89. db.components().insertPrivateProject(p -> p.setKey("project-key-without-escaped-characters")).getMainBranchComponent();
  90. SearchWsResponse response = call(SearchRequest.builder().setQuery("JeCt-_%-k").build());
  91. assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly("project-_%-key", "PROJECT-_%-KEY");
  92. }
  93. @Test
  94. public void search_private_projects() {
  95. userSession.addPermission(ADMINISTER);
  96. db.components().insertPrivateProject(p -> p.setKey("private-key")).getMainBranchComponent();
  97. db.components().insertPublicProject(p -> p.setKey("public-key")).getMainBranchComponent();
  98. SearchWsResponse response = call(SearchRequest.builder().setVisibility("private").build());
  99. assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly("private-key");
  100. }
  101. @Test
  102. public void search_public_projects() {
  103. userSession.addPermission(ADMINISTER);
  104. db.components().insertPrivateProject(p -> p.setKey("private-key")).getMainBranchComponent();
  105. db.components().insertPublicProject(p -> p.setKey("public-key")).getMainBranchComponent();
  106. SearchWsResponse response = call(SearchRequest.builder().setVisibility("public").build());
  107. assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly("public-key");
  108. }
  109. @Test
  110. public void search_projects_when_no_qualifier_set() {
  111. userSession.addPermission(ADMINISTER);
  112. db.components().insertPrivateProject(p -> p.setKey(PROJECT_KEY_1)).getMainBranchComponent();
  113. db.components().insertPublicPortfolio();
  114. SearchWsResponse response = call(SearchRequest.builder().build());
  115. assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(PROJECT_KEY_1);
  116. }
  117. @Test
  118. public void search_projects() {
  119. userSession.addPermission(ADMINISTER);
  120. ProjectData project = db.components().insertPrivateProject(p -> p.setKey(PROJECT_KEY_1));
  121. db.components().insertPrivateProject(p -> p.setKey(PROJECT_KEY_2));
  122. db.components().insertPublicPortfolio();
  123. ComponentDto directory = newDirectory(project.getMainBranchComponent(), "dir");
  124. ComponentDto file = newFileDto(directory);
  125. db.components().insertComponents(directory, file);
  126. SearchWsResponse response = call(SearchRequest.builder().setQualifiers(singletonList("TRK")).build());
  127. assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(PROJECT_KEY_1, PROJECT_KEY_2);
  128. }
  129. @Test
  130. public void search_views() {
  131. userSession.addPermission(ADMINISTER);
  132. db.components().insertPrivateProject(p -> p.setKey(PROJECT_KEY_1));
  133. db.components().insertPublicPortfolio(p -> p.setKey("view1"));
  134. SearchWsResponse response = call(SearchRequest.builder().setQualifiers(singletonList("VW")).build());
  135. assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly("view1");
  136. }
  137. @Test
  138. public void search_projects_and_views() {
  139. userSession.addPermission(ADMINISTER);
  140. db.components().insertPrivateProject(p -> p.setKey(PROJECT_KEY_1));
  141. db.components().insertPublicPortfolio(p -> p.setKey("view1"));
  142. SearchWsResponse response = call(SearchRequest.builder().setQualifiers(asList("TRK", "VW")).build());
  143. assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(PROJECT_KEY_1, "view1");
  144. }
  145. @Test
  146. public void search_all() {
  147. userSession.addPermission(ADMINISTER);
  148. db.components().insertPrivateProject(p -> p.setKey(PROJECT_KEY_1));
  149. db.components().insertPrivateProject(p -> p.setKey(PROJECT_KEY_2));
  150. db.components().insertPrivateProject(p -> p.setKey(PROJECT_KEY_3));
  151. SearchWsResponse response = call(SearchRequest.builder().build());
  152. assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(PROJECT_KEY_1, PROJECT_KEY_2, PROJECT_KEY_3);
  153. }
  154. @Test
  155. public void search_for_old_projects() {
  156. userSession.addPermission(ADMINISTER);
  157. long aLongTimeAgo = 1_000_000_000L;
  158. long inBetween = 2_000_000_000L;
  159. long recentTime = 3_000_000_000L;
  160. ProjectData oldProject = db.components().insertPublicProject();
  161. db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(oldProject.getMainBranchDto()).setCreatedAt(aLongTimeAgo));
  162. BranchDto branch = db.components().insertProjectBranch(oldProject.getProjectDto());
  163. db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(branch).setCreatedAt(inBetween));
  164. ProjectData recentProject = db.components().insertPublicProject();
  165. db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(recentProject.getMainBranchDto()).setCreatedAt(recentTime));
  166. db.commit();
  167. SearchWsResponse result = call(SearchRequest.builder().setAnalyzedBefore(toStringAtUTC(new Date(recentTime + 1_000))).build());
  168. assertThat(result.getComponentsList()).extracting(Component::getKey, Component::getLastAnalysisDate)
  169. .containsExactlyInAnyOrder(tuple(oldProject.getProjectDto().getKey(), formatDateTime(inBetween)), tuple(recentProject.projectKey(), formatDateTime(recentTime)));
  170. result = call(SearchRequest.builder().setAnalyzedBefore(toStringAtUTC(new Date(recentTime))).build());
  171. assertThat(result.getComponentsList()).extracting(Component::getKey, Component::getLastAnalysisDate)
  172. .containsExactlyInAnyOrder(tuple(oldProject.getProjectDto().getKey(), formatDateTime(inBetween)));
  173. result = call(SearchRequest.builder().setAnalyzedBefore(toStringAtUTC(new Date(aLongTimeAgo + 1_000L))).build());
  174. assertThat(result.getComponentsList()).isEmpty();
  175. }
  176. @Test
  177. public void search_return_manage_status() {
  178. userSession.addPermission(ADMINISTER);
  179. ProjectData managedProject = db.components().insertPrivateProject();
  180. ProjectData notManagedProject = db.components().insertPrivateProject();
  181. when(managedProjectService.getProjectUuidToManaged(any(), eq(Set.of(managedProject.projectUuid(), notManagedProject.projectUuid()))))
  182. .thenReturn(Map.of(
  183. managedProject.projectUuid(), true,
  184. notManagedProject.projectUuid(), false
  185. ));
  186. SearchWsResponse result = call(SearchRequest.builder().build());
  187. assertThat(result.getComponentsList())
  188. .extracting(Component::getKey, Component::getManaged)
  189. .containsExactlyInAnyOrder(
  190. tuple(managedProject.projectKey(), true),
  191. tuple(notManagedProject.projectKey(), false));
  192. }
  193. private static String toStringAtUTC(Date d) {
  194. OffsetDateTime offsetTime = d.toInstant().atOffset(ZoneOffset.UTC);
  195. return DateUtils.formatDateTime(offsetTime);
  196. }
  197. @Test
  198. public void does_not_return_branches_when_searching_by_key() {
  199. ProjectDto project = db.components().insertPublicProject().getProjectDto();
  200. db.components().insertProjectBranch(project);
  201. userSession.addPermission(ADMINISTER);
  202. SearchWsResponse response = call(SearchRequest.builder().build());
  203. assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(project.getKey());
  204. }
  205. @Test
  206. public void result_is_paginated() {
  207. userSession.addPermission(ADMINISTER);
  208. for (int i = 1; i <= 9; i++) {
  209. int j = i;
  210. db.components().insertPrivateProject("project-uuid-" + i, p -> p.setKey("project-key-" + j).setName("Project Name " + j));
  211. }
  212. SearchWsResponse response = call(SearchRequest.builder().setPage(2).setPageSize(3).build());
  213. assertThat(response.getComponentsList()).extracting(Component::getKey).containsExactly("project-key-4", "project-key-5", "project-key-6");
  214. }
  215. @Test
  216. public void provisioned_projects() {
  217. userSession.addPermission(ADMINISTER);
  218. ProjectDto provisionedProject = db.components().insertPrivateProject().getProjectDto();
  219. ProjectData analyzedProject = db.components().insertPrivateProject();
  220. db.components().insertSnapshot(newAnalysis(analyzedProject.getMainBranchDto()));
  221. SearchWsResponse response = call(SearchRequest.builder().setOnProvisionedOnly(true).build());
  222. assertThat(response.getComponentsList()).extracting(Component::getKey)
  223. .containsExactlyInAnyOrder(provisionedProject.getKey())
  224. .doesNotContain(analyzedProject.projectKey());
  225. }
  226. @Test
  227. public void search_by_component_keys() {
  228. userSession.addPermission(ADMINISTER);
  229. ProjectDto jdk = db.components().insertPrivateProject().getProjectDto();
  230. ProjectDto sonarqube = db.components().insertPrivateProject().getProjectDto();
  231. ProjectDto sonarlint = db.components().insertPrivateProject().getProjectDto();
  232. SearchWsResponse result = call(SearchRequest.builder()
  233. .setProjects(Arrays.asList(jdk.getKey(), sonarqube.getKey()))
  234. .build());
  235. assertThat(result.getComponentsList()).extracting(Component::getKey)
  236. .containsExactlyInAnyOrder(jdk.getKey(), sonarqube.getKey())
  237. .doesNotContain(sonarlint.getKey());
  238. }
  239. @Test
  240. public void request_throws_IAE_if_more_than_1000_projects() {
  241. SearchRequest request = SearchRequest.builder()
  242. .setProjects(Collections.nCopies(1_001, "foo"))
  243. .build();
  244. assertThatThrownBy(() -> call(request))
  245. .isInstanceOf(IllegalArgumentException.class)
  246. .hasMessage("'projects' can contains only 1000 values, got 1001");
  247. }
  248. @Test
  249. public void fail_when_not_system_admin() {
  250. userSession.addPermission(ADMINISTER_QUALITY_PROFILES);
  251. SearchRequest request = SearchRequest.builder().build();
  252. assertThatThrownBy(() -> call(request))
  253. .isInstanceOf(ForbiddenException.class);
  254. }
  255. @Test
  256. public void fail_on_invalid_qualifier() {
  257. userSession.addPermission(ADMINISTER_QUALITY_PROFILES);
  258. SearchRequest request = SearchRequest.builder().setQualifiers(singletonList("BRC")).build();
  259. assertThatThrownBy(() -> call(request))
  260. .isInstanceOf(IllegalArgumentException.class)
  261. .hasMessage("Value of parameter 'qualifiers' (BRC) must be one of: [TRK, VW, APP]");
  262. }
  263. @Test
  264. public void definition() {
  265. WebService.Action action = ws.getDef();
  266. assertThat(action.key()).isEqualTo("search");
  267. assertThat(action.isPost()).isFalse();
  268. assertThat(action.description()).isEqualTo("Search for projects or views to administrate them.<br>Requires 'Administer System' permission");
  269. assertThat(action.isInternal()).isFalse();
  270. assertThat(action.since()).isEqualTo("6.3");
  271. assertThat(action.handler()).isEqualTo(ws.getDef().handler());
  272. assertThat(action.params()).extracting(Param::key)
  273. .containsExactlyInAnyOrder("q", "qualifiers", "p", "ps", "visibility", "analyzedBefore", "onProvisionedOnly", "projects");
  274. assertThat(action.responseExample()).isEqualTo(getClass().getResource("search-example.json"));
  275. Param qParam = action.param("q");
  276. assertThat(qParam.isRequired()).isFalse();
  277. assertThat(qParam.description()).isEqualTo("Limit search to: " +
  278. "<ul>" +
  279. "<li>component names that contain the supplied string</li>" +
  280. "<li>component keys that contain the supplied string</li>" +
  281. "</ul>");
  282. Param qualifierParam = action.param("qualifiers");
  283. assertThat(qualifierParam.isRequired()).isFalse();
  284. assertThat(qualifierParam.description()).isEqualTo("Comma-separated list of component qualifiers. Filter the results with the specified qualifiers");
  285. assertThat(qualifierParam.possibleValues()).containsOnly("TRK", "VW", "APP");
  286. assertThat(qualifierParam.defaultValue()).isEqualTo("TRK");
  287. Param pParam = action.param("p");
  288. assertThat(pParam.isRequired()).isFalse();
  289. assertThat(pParam.defaultValue()).isEqualTo("1");
  290. assertThat(pParam.description()).isEqualTo("1-based page number");
  291. Param psParam = action.param("ps");
  292. assertThat(psParam.isRequired()).isFalse();
  293. assertThat(psParam.defaultValue()).isEqualTo("100");
  294. assertThat(psParam.description()).isEqualTo("Page size. Must be greater than 0 and less or equal than 500");
  295. Param visibilityParam = action.param("visibility");
  296. assertThat(visibilityParam.isRequired()).isFalse();
  297. assertThat(visibilityParam.description()).isEqualTo("Filter the projects that should be visible to everyone (public), or only specific user/groups (private).<br/>" +
  298. "If no visibility is specified, the default project visibility will be used.");
  299. Param lastAnalysisBefore = action.param("analyzedBefore");
  300. assertThat(lastAnalysisBefore.isRequired()).isFalse();
  301. assertThat(lastAnalysisBefore.since()).isEqualTo("6.6");
  302. Param onProvisionedOnly = action.param("onProvisionedOnly");
  303. assertThat(onProvisionedOnly.possibleValues()).containsExactlyInAnyOrder("true", "false", "yes", "no");
  304. assertThat(onProvisionedOnly.defaultValue()).isEqualTo("false");
  305. assertThat(onProvisionedOnly.since()).isEqualTo("6.6");
  306. }
  307. @Test
  308. public void json_example() {
  309. userSession.addPermission(ADMINISTER);
  310. ProjectData publicProject = db.components().insertPublicProject("project-uuid-1", p -> p.setName("Project Name 1").setKey("project-key-1").setPrivate(false));
  311. ProjectData privateProject = db.components().insertPrivateProject("project-uuid-2", p -> p.setName("Project Name 2").setKey("project-key-2"));
  312. db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(publicProject.getMainBranchDto())
  313. .setCreatedAt(parseDateTime("2017-03-01T11:39:03+0300").getTime())
  314. .setRevision("cfb82f55c6ef32e61828c4cb3db2da12795fd767"));
  315. db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(privateProject.getMainBranchDto())
  316. .setCreatedAt(parseDateTime("2017-03-02T15:21:47+0300").getTime())
  317. .setRevision("7be96a94ac0c95a61ee6ee0ef9c6f808d386a355"));
  318. db.commit();
  319. String response = ws.newRequest()
  320. .setMediaType(MediaTypes.JSON)
  321. .execute().getInput();
  322. assertJson(response).isSimilarTo(ws.getDef().responseExampleAsString());
  323. assertJson(ws.getDef().responseExampleAsString()).isSimilarTo(response);
  324. }
  325. private SearchWsResponse call(SearchRequest wsRequest) {
  326. TestRequest request = ws.newRequest();
  327. List<String> qualifiers = wsRequest.getQualifiers();
  328. if (!qualifiers.isEmpty()) {
  329. request.setParam(ComponentsWsParameters.PARAM_QUALIFIERS, Joiner.on(",").join(qualifiers));
  330. }
  331. ofNullable(wsRequest.getQuery()).ifPresent(query -> request.setParam(TEXT_QUERY, query));
  332. ofNullable(wsRequest.getPage()).ifPresent(page -> request.setParam(PAGE, String.valueOf(page)));
  333. ofNullable(wsRequest.getPageSize()).ifPresent(pageSize -> request.setParam(PAGE_SIZE, String.valueOf(pageSize)));
  334. ofNullable(wsRequest.getVisibility()).ifPresent(v -> request.setParam(PARAM_VISIBILITY, v));
  335. ofNullable(wsRequest.getAnalyzedBefore()).ifPresent(d -> request.setParam(PARAM_ANALYZED_BEFORE, d));
  336. ofNullable(wsRequest.getProjects()).ifPresent(l1 -> request.setParam(PARAM_PROJECTS, String.join(",", l1)));
  337. request.setParam(PARAM_ON_PROVISIONED_ONLY, String.valueOf(wsRequest.isOnProvisionedOnly()));
  338. return request.executeProtobuf(SearchWsResponse.class);
  339. }
  340. }