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.

SearchActionFacetsTest.java 20KB

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