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.

SearchActionFacetsIT.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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.issue.ws;
  21. import com.google.common.collect.ImmutableMap;
  22. import java.time.Clock;
  23. import java.util.Map;
  24. import java.util.Random;
  25. import java.util.stream.IntStream;
  26. import org.junit.Rule;
  27. import org.junit.Test;
  28. import org.sonar.api.issue.Issue;
  29. import org.sonar.api.resources.Languages;
  30. import org.sonar.api.rules.RuleType;
  31. import org.sonar.api.server.ws.WebService;
  32. import org.sonar.api.utils.Durations;
  33. import org.sonar.api.utils.System2;
  34. import org.sonar.db.DbTester;
  35. import org.sonar.db.component.ComponentDto;
  36. import org.sonar.db.rule.RuleDto;
  37. import org.sonar.db.user.UserDto;
  38. import org.sonar.server.es.EsTester;
  39. import org.sonar.server.issue.AvatarResolverImpl;
  40. import org.sonar.server.issue.TextRangeResponseFormatter;
  41. import org.sonar.server.issue.TransitionService;
  42. import org.sonar.server.issue.index.IssueIndex;
  43. import org.sonar.server.issue.index.IssueIndexSyncProgressChecker;
  44. import org.sonar.server.issue.index.IssueIndexer;
  45. import org.sonar.server.issue.index.IssueIteratorFactory;
  46. import org.sonar.server.issue.index.IssueQueryFactory;
  47. import org.sonar.server.permission.index.PermissionIndexer;
  48. import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
  49. import org.sonar.server.tester.UserSessionRule;
  50. import org.sonar.server.ws.WsActionTester;
  51. import org.sonarqube.ws.Common;
  52. import org.sonarqube.ws.Common.FacetValue;
  53. import org.sonarqube.ws.Issues.SearchWsResponse;
  54. import static com.google.common.collect.ImmutableMap.of;
  55. import static java.util.stream.Collectors.toMap;
  56. import static org.assertj.core.api.Assertions.assertThat;
  57. import static org.assertj.core.api.Assertions.assertThatThrownBy;
  58. import static org.assertj.core.groups.Tuple.tuple;
  59. import static org.sonar.api.server.ws.WebService.Param.FACETS;
  60. import static org.sonar.db.component.ComponentTesting.newDirectory;
  61. import static org.sonar.db.component.ComponentTesting.newFileDto;
  62. import static org.sonar.server.tester.UserSessionRule.standalone;
  63. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENTS;
  64. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES;
  65. import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECTS;
  66. public class SearchActionFacetsIT {
  67. private static final String[] ISSUE_STATUSES = Issue.STATUSES.stream().filter(s -> !Issue.STATUS_TO_REVIEW.equals(s)).filter(s -> !Issue.STATUS_REVIEWED.equals(s))
  68. .toArray(String[]::new);
  69. @Rule
  70. public UserSessionRule userSession = standalone();
  71. @Rule
  72. public DbTester db = DbTester.create();
  73. @Rule
  74. public EsTester es = EsTester.create();
  75. private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new WebAuthorizationTypeSupport(userSession));
  76. private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()), null);
  77. private PermissionIndexer permissionIndexer = new PermissionIndexer(db.getDbClient(), es.client(), issueIndexer);
  78. private IssueQueryFactory issueQueryFactory = new IssueQueryFactory(db.getDbClient(), Clock.systemUTC(), userSession);
  79. private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, db.getDbClient(), new TransitionService(userSession, null));
  80. private Languages languages = new Languages();
  81. private UserResponseFormatter userFormatter = new UserResponseFormatter(new AvatarResolverImpl());
  82. private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new TextRangeResponseFormatter(), userFormatter);
  83. private IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = new IssueIndexSyncProgressChecker(db.getDbClient());
  84. private WsActionTester ws = new WsActionTester(
  85. new SearchAction(userSession, issueIndex, issueQueryFactory, issueIndexSyncProgressChecker, searchResponseLoader, searchResponseFormat, System2.INSTANCE, db.getDbClient()));
  86. @Test
  87. public void display_all_facets() {
  88. ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
  89. ComponentDto file = db.components().insertComponent(newFileDto(project));
  90. RuleDto rule = db.rules().insertIssueRule();
  91. UserDto user = db.users().insertUser();
  92. db.issues().insertIssue(rule, project, file, i -> i
  93. .setSeverity("MAJOR")
  94. .setStatus("OPEN")
  95. .setType(RuleType.CODE_SMELL)
  96. .setEffort(10L)
  97. .setAssigneeUuid(user.getUuid()));
  98. indexPermissions();
  99. indexIssues();
  100. SearchWsResponse response = ws.newRequest()
  101. .setParam(PARAM_COMPONENTS, project.getKey())
  102. .setParam(FACETS, "severities,statuses,resolutions,rules,types,languages,projects,files,assignees")
  103. .executeProtobuf(SearchWsResponse.class);
  104. Map<String, Number> expectedStatuses = ImmutableMap.<String, Number>builder().put("OPEN", 1L).put("CONFIRMED", 0L)
  105. .put("REOPENED", 0L).put("RESOLVED", 0L).put("CLOSED", 0L).build();
  106. assertThat(response.getFacets().getFacetsList())
  107. .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
  108. .containsExactlyInAnyOrder(
  109. tuple("severities", of("INFO", 0L, "MINOR", 0L, "MAJOR", 1L, "CRITICAL", 0L, "BLOCKER", 0L)),
  110. tuple("statuses", expectedStatuses),
  111. tuple("resolutions", of("", 1L, "FALSE-POSITIVE", 0L, "FIXED", 0L, "REMOVED", 0L, "WONTFIX", 0L)),
  112. tuple("rules", of(rule.getKey().toString(), 1L)),
  113. tuple("types", of("CODE_SMELL", 1L, "BUG", 0L, "VULNERABILITY", 0L)),
  114. tuple("languages", of(rule.getLanguage(), 1L)),
  115. tuple("projects", of(project.getKey(), 1L)),
  116. tuple("files", of(file.path(), 1L)),
  117. tuple("assignees", of("", 0L, user.getLogin(), 1L)));
  118. }
  119. @Test
  120. public void display_projects_facet() {
  121. ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
  122. ComponentDto file = db.components().insertComponent(newFileDto(project));
  123. RuleDto rule = db.rules().insertIssueRule();
  124. db.issues().insertIssue(rule, project, file);
  125. indexPermissions();
  126. indexIssues();
  127. SearchWsResponse response = ws.newRequest()
  128. .setParam(PARAM_PROJECTS, project.getKey())
  129. .setParam(WebService.Param.FACETS, "projects")
  130. .executeProtobuf(SearchWsResponse.class);
  131. assertThat(response.getFacets().getFacetsList())
  132. .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
  133. .containsExactlyInAnyOrder(tuple("projects", of(project.getKey(), 1L)));
  134. }
  135. @Test
  136. public void projects_facet_is_sticky() {
  137. ComponentDto project1 = db.components().insertPublicProject().getMainBranchComponent();
  138. ComponentDto project2 = db.components().insertPublicProject().getMainBranchComponent();
  139. ComponentDto project3 = db.components().insertPublicProject().getMainBranchComponent();
  140. ComponentDto file1 = db.components().insertComponent(newFileDto(project1));
  141. ComponentDto file2 = db.components().insertComponent(newFileDto(project2));
  142. ComponentDto file3 = db.components().insertComponent(newFileDto(project3));
  143. RuleDto rule = db.rules().insertIssueRule();
  144. db.issues().insertIssue(rule, project1, file1);
  145. db.issues().insertIssue(rule, project2, file2);
  146. db.issues().insertIssue(rule, project3, file3);
  147. indexPermissions();
  148. indexIssues();
  149. SearchWsResponse response = ws.newRequest()
  150. .setParam(PARAM_PROJECTS, project1.getKey())
  151. .setParam(WebService.Param.FACETS, "projects")
  152. .executeProtobuf(SearchWsResponse.class);
  153. assertThat(response.getFacets().getFacetsList())
  154. .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
  155. .containsExactlyInAnyOrder(tuple("projects", of(project1.getKey(), 1L, project2.getKey(), 1L, project3.getKey(), 1L)));
  156. }
  157. @Test
  158. public void display_directory_facet_using_project() {
  159. ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
  160. ComponentDto directory = db.components().insertComponent(newDirectory(project, "src/main/java/dir"));
  161. ComponentDto file = db.components().insertComponent(newFileDto(project, directory));
  162. RuleDto rule = db.rules().insertIssueRule();
  163. db.issues().insertIssue(rule, project, file);
  164. indexPermissions();
  165. indexIssues();
  166. SearchWsResponse response = ws.newRequest()
  167. .setParam("resolved", "false")
  168. .setParam(PARAM_COMPONENTS, project.getKey())
  169. .setParam(WebService.Param.FACETS, "directories")
  170. .executeProtobuf(SearchWsResponse.class);
  171. assertThat(response.getFacets().getFacetsList())
  172. .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
  173. .containsExactlyInAnyOrder(tuple("directories", of(directory.path(), 1L)));
  174. }
  175. @Test
  176. public void fail_to_display_directory_facet_when_no_project_is_set() {
  177. ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
  178. ComponentDto directory = db.components().insertComponent(newDirectory(project, "src"));
  179. ComponentDto file = db.components().insertComponent(newFileDto(project, directory));
  180. RuleDto rule = db.rules().insertIssueRule();
  181. db.issues().insertIssue(rule, project, file);
  182. indexPermissions();
  183. indexIssues();
  184. assertThatThrownBy(() -> {
  185. ws.newRequest()
  186. .setParam(WebService.Param.FACETS, "directories")
  187. .execute();
  188. })
  189. .isInstanceOf(IllegalArgumentException.class)
  190. .hasMessage("Facet(s) 'directories' require to also filter by project");
  191. }
  192. @Test
  193. public void display_files_facet_with_project() {
  194. ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
  195. ComponentDto file1 = db.components().insertComponent(newFileDto(project));
  196. ComponentDto file2 = db.components().insertComponent(newFileDto(project));
  197. ComponentDto file3 = db.components().insertComponent(newFileDto(project));
  198. RuleDto rule = db.rules().insertIssueRule();
  199. db.issues().insertIssue(rule, project, file1);
  200. db.issues().insertIssue(rule, project, file2);
  201. indexPermissions();
  202. indexIssues();
  203. SearchWsResponse response = ws.newRequest()
  204. .setParam(PARAM_COMPONENTS, project.getKey())
  205. .setParam(PARAM_FILES, file1.path())
  206. .setParam(WebService.Param.FACETS, "files")
  207. .executeProtobuf(SearchWsResponse.class);
  208. assertThat(response.getFacets().getFacetsList())
  209. .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
  210. .containsExactlyInAnyOrder(tuple("files", of(file1.path(), 1L, file2.path(), 1L)));
  211. }
  212. @Test
  213. public void fail_to_display_fileUuids_facet_when_no_project_is_set() {
  214. ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
  215. ComponentDto file = db.components().insertComponent(newFileDto(project));
  216. RuleDto rule = db.rules().insertIssueRule();
  217. db.issues().insertIssue(rule, project, file);
  218. indexPermissions();
  219. indexIssues();
  220. assertThatThrownBy(() -> {
  221. ws.newRequest()
  222. .setParam(PARAM_FILES, file.path())
  223. .setParam(WebService.Param.FACETS, "files")
  224. .execute();
  225. })
  226. .isInstanceOf(IllegalArgumentException.class)
  227. .hasMessage("Facet(s) 'files' require to also filter by project");
  228. }
  229. @Test
  230. public void check_facets_max_size_for_issues() {
  231. ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
  232. Random random = new Random();
  233. IntStream.rangeClosed(1, 110)
  234. .forEach(index -> {
  235. UserDto user = db.users().insertUser();
  236. ComponentDto directory = db.components().insertComponent(newDirectory(project, "dir" + index));
  237. ComponentDto file = db.components().insertComponent(newFileDto(project, directory));
  238. RuleDto rule = db.rules().insertIssueRule();
  239. db.issues().insertIssue(rule, project, file, i -> i.setAssigneeUuid(user.getUuid())
  240. .setStatus(ISSUE_STATUSES[random.nextInt(ISSUE_STATUSES.length)])
  241. .setType(rule.getType()));
  242. });
  243. // insert some hotspots which should be filtered by default
  244. IntStream.rangeClosed(201, 230)
  245. .forEach(index -> {
  246. UserDto user = db.users().insertUser();
  247. ComponentDto directory = db.components().insertComponent(newDirectory(project, "dir" + index));
  248. ComponentDto file = db.components().insertComponent(newFileDto(project, directory));
  249. db.issues().insertHotspot(project, file, i -> i.setAssigneeUuid(user.getUuid())
  250. .setStatus(random.nextBoolean() ? Issue.STATUS_TO_REVIEW : Issue.STATUS_REVIEWED));
  251. });
  252. indexPermissions();
  253. indexIssues();
  254. SearchWsResponse response = ws.newRequest()
  255. .setParam(PARAM_COMPONENTS, project.getKey())
  256. .setParam(FACETS, "files,directories,statuses,resolutions,severities,types,rules,languages,assignees")
  257. .executeProtobuf(SearchWsResponse.class);
  258. assertThat(response.getFacets().getFacetsList())
  259. .extracting(Common.Facet::getProperty, Common.Facet::getValuesCount)
  260. .containsExactlyInAnyOrder(
  261. tuple("files", 100),
  262. tuple("directories", 100),
  263. tuple("rules", 100),
  264. tuple("languages", 100),
  265. // Assignees contains one additional element : it's the empty string that will return number of unassigned issues
  266. tuple("assignees", 101),
  267. // Following facets returned fixed number of elements
  268. tuple("statuses", 5),
  269. tuple("resolutions", 5),
  270. tuple("severities", 5),
  271. tuple("types", 3));
  272. }
  273. @Test
  274. public void check_projects_facet_max_size() {
  275. RuleDto rule = db.rules().insertIssueRule();
  276. IntStream.rangeClosed(1, 110)
  277. .forEach(i -> {
  278. ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
  279. db.issues().insertIssue(rule, project, project);
  280. });
  281. indexPermissions();
  282. indexIssues();
  283. SearchWsResponse response = ws.newRequest()
  284. .setParam(FACETS, "projects")
  285. .executeProtobuf(SearchWsResponse.class);
  286. assertThat(response.getPaging().getTotal()).isEqualTo(110);
  287. assertThat(response.getFacets().getFacets(0).getValuesCount()).isEqualTo(100);
  288. }
  289. @Test
  290. public void display_zero_valued_facets_for_selected_items_having_no_issue() {
  291. ComponentDto project1 = db.components().insertPublicProject().getMainBranchComponent();
  292. ComponentDto project2 = db.components().insertPublicProject().getMainBranchComponent();
  293. ComponentDto file1 = db.components().insertComponent(newFileDto(project1));
  294. ComponentDto file2 = db.components().insertComponent(newFileDto(project1));
  295. RuleDto rule1 = db.rules().insertIssueRule();
  296. RuleDto rule2 = db.rules().insertIssueRule();
  297. UserDto user1 = db.users().insertUser();
  298. UserDto user2 = db.users().insertUser();
  299. db.issues().insertIssue(rule1, project1, file1, i -> i
  300. .setSeverity("MAJOR")
  301. .setStatus("OPEN")
  302. .setResolution(null)
  303. .setType(RuleType.CODE_SMELL)
  304. .setEffort(10L)
  305. .setAssigneeUuid(user1.getUuid()));
  306. indexPermissions();
  307. indexIssues();
  308. SearchWsResponse response = ws.newRequest()
  309. .setParam(PARAM_PROJECTS, project1.getKey() + "," + project2.getKey())
  310. .setParam(PARAM_FILES, file1.path() + "," + file2.path())
  311. .setParam("rules", rule1.getKey().toString() + "," + rule2.getKey().toString())
  312. .setParam("severities", "MAJOR,MINOR")
  313. .setParam("languages", rule1.getLanguage() + "," + rule2.getLanguage())
  314. .setParam("assignees", user1.getLogin() + "," + user2.getLogin())
  315. .setParam(FACETS, "severities,statuses,resolutions,rules,types,languages,projects,files,assignees")
  316. .executeProtobuf(SearchWsResponse.class);
  317. Map<String, Number> expectedStatuses = ImmutableMap.<String, Number>builder().put("OPEN", 1L).put("CONFIRMED", 0L)
  318. .put("REOPENED", 0L).put("RESOLVED", 0L).put("CLOSED", 0L).build();
  319. assertThat(response.getFacets().getFacetsList())
  320. .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
  321. .containsExactlyInAnyOrder(
  322. tuple("severities", of("INFO", 0L, "MINOR", 0L, "MAJOR", 1L, "CRITICAL", 0L, "BLOCKER", 0L)),
  323. tuple("statuses", expectedStatuses),
  324. tuple("resolutions", of("", 1L, "FALSE-POSITIVE", 0L, "FIXED", 0L, "REMOVED", 0L, "WONTFIX", 0L)),
  325. tuple("rules", of(rule1.getKey().toString(), 1L, rule2.getKey().toString(), 0L)),
  326. tuple("types", of("CODE_SMELL", 1L, "BUG", 0L, "VULNERABILITY", 0L)),
  327. tuple("languages", of(rule1.getLanguage(), 1L, rule2.getLanguage(), 0L)),
  328. tuple("projects", of(project1.getKey(), 1L, project2.getKey(), 0L)),
  329. tuple("files", of(file1.path(), 1L, file2.path(), 0L)),
  330. tuple("assignees", of("", 0L, user1.getLogin(), 1L, user2.getLogin(), 0L)));
  331. }
  332. @Test
  333. public void assignedToMe_facet_must_escape_login_of_authenticated_user() {
  334. // login looks like an invalid regexp
  335. UserDto user = db.users().insertUser(u -> u.setLogin("foo["));
  336. userSession.logIn(user);
  337. // should not fail
  338. SearchWsResponse response = ws.newRequest()
  339. .setParam(FACETS, "assigned_to_me")
  340. .executeProtobuf(SearchWsResponse.class);
  341. assertThat(response.getFacets().getFacetsList())
  342. .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
  343. .containsExactlyInAnyOrder(
  344. tuple("assigned_to_me", of("foo[", 0L)));
  345. }
  346. @Test
  347. public void assigned_to_me_facet_is_sticky_relative_to_assignees() {
  348. ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
  349. indexPermissions();
  350. ComponentDto file = db.components().insertComponent(newFileDto(project));
  351. RuleDto rule = db.rules().insertIssueRule();
  352. UserDto john = db.users().insertUser();
  353. UserDto alice = db.users().insertUser();
  354. db.issues().insertIssue(rule, project, file, i -> i.setAssigneeUuid(john.getUuid()));
  355. db.issues().insertIssue(rule, project, file, i -> i.setAssigneeUuid(alice.getUuid()));
  356. db.issues().insertIssue(rule, project, file, i -> i.setAssigneeUuid(null));
  357. indexIssues();
  358. userSession.logIn(john);
  359. SearchWsResponse response = ws.newRequest()
  360. .setParam("resolved", "false")
  361. .setParam("assignees", alice.getLogin())
  362. .setParam(FACETS, "assignees,assigned_to_me")
  363. .executeProtobuf(SearchWsResponse.class);
  364. assertThat(response.getFacets().getFacetsList())
  365. .extracting(Common.Facet::getProperty, facet -> facet.getValuesList().stream().collect(toMap(FacetValue::getVal, FacetValue::getCount)))
  366. .containsExactlyInAnyOrder(
  367. tuple("assignees", of(john.getLogin(), 1L, alice.getLogin(), 1L, "", 1L)),
  368. tuple("assigned_to_me", of(john.getLogin(), 1L)));
  369. }
  370. private void indexPermissions() {
  371. permissionIndexer.indexAll(permissionIndexer.getIndexTypes());
  372. }
  373. private void indexIssues() {
  374. issueIndexer.indexAllIssues();
  375. }
  376. }