Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

SendIssueNotificationsStepTest.java 39KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  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.ce.task.projectanalysis.step;
  21. import com.google.common.collect.ImmutableMap;
  22. import com.google.common.collect.ImmutableSet;
  23. import java.io.IOException;
  24. import java.util.ArrayList;
  25. import java.util.Collection;
  26. import java.util.Date;
  27. import java.util.HashMap;
  28. import java.util.List;
  29. import java.util.Map;
  30. import java.util.Random;
  31. import java.util.Set;
  32. import java.util.function.Supplier;
  33. import java.util.stream.IntStream;
  34. import java.util.stream.Stream;
  35. import org.assertj.core.groups.Tuple;
  36. import org.junit.Before;
  37. import org.junit.Rule;
  38. import org.junit.Test;
  39. import org.junit.rules.TemporaryFolder;
  40. import org.mockito.ArgumentCaptor;
  41. import org.mockito.invocation.InvocationOnMock;
  42. import org.mockito.stubbing.Answer;
  43. import org.sonar.api.notifications.Notification;
  44. import org.sonar.api.rule.RuleKey;
  45. import org.sonar.api.rules.RuleType;
  46. import org.sonar.api.utils.Duration;
  47. import org.sonar.api.utils.System2;
  48. import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule;
  49. import org.sonar.ce.task.projectanalysis.analysis.Branch;
  50. import org.sonar.ce.task.projectanalysis.component.Component;
  51. import org.sonar.ce.task.projectanalysis.component.DefaultBranchImpl;
  52. import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
  53. import org.sonar.ce.task.projectanalysis.issue.ProtoIssueCache;
  54. import org.sonar.ce.task.projectanalysis.notification.NotificationFactory;
  55. import org.sonar.ce.task.projectanalysis.util.cache.DiskCache;
  56. import org.sonar.ce.task.step.ComputationStep;
  57. import org.sonar.ce.task.step.TestComputationStepContext;
  58. import org.sonar.core.issue.DefaultIssue;
  59. import org.sonar.db.DbTester;
  60. import org.sonar.db.component.BranchType;
  61. import org.sonar.db.component.ComponentDto;
  62. import org.sonar.db.rule.RuleDefinitionDto;
  63. import org.sonar.db.user.UserDto;
  64. import org.sonar.server.issue.notification.DistributedMetricStatsInt;
  65. import org.sonar.server.issue.notification.IssuesChangesNotification;
  66. import org.sonar.server.issue.notification.MyNewIssuesNotification;
  67. import org.sonar.server.issue.notification.NewIssuesNotification;
  68. import org.sonar.server.issue.notification.NewIssuesStatistics;
  69. import org.sonar.server.notification.NotificationService;
  70. import org.sonar.server.project.Project;
  71. import static java.util.Arrays.stream;
  72. import static java.util.Collections.emptyList;
  73. import static java.util.Collections.shuffle;
  74. import static java.util.Collections.singleton;
  75. import static java.util.stream.Collectors.toList;
  76. import static java.util.stream.Stream.concat;
  77. import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
  78. import static org.apache.commons.lang.math.RandomUtils.nextInt;
  79. import static org.assertj.core.api.Assertions.assertThat;
  80. import static org.assertj.core.groups.Tuple.tuple;
  81. import static org.mockito.ArgumentCaptor.forClass;
  82. import static org.mockito.ArgumentMatchers.anyCollection;
  83. import static org.mockito.ArgumentMatchers.anyMap;
  84. import static org.mockito.ArgumentMatchers.anySet;
  85. import static org.mockito.ArgumentMatchers.eq;
  86. import static org.mockito.Mockito.any;
  87. import static org.mockito.Mockito.doReturn;
  88. import static org.mockito.Mockito.mock;
  89. import static org.mockito.Mockito.never;
  90. import static org.mockito.Mockito.times;
  91. import static org.mockito.Mockito.verify;
  92. import static org.mockito.Mockito.verifyNoMoreInteractions;
  93. import static org.mockito.Mockito.verifyZeroInteractions;
  94. import static org.mockito.Mockito.when;
  95. import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
  96. import static org.sonar.ce.task.projectanalysis.component.Component.Type;
  97. import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
  98. import static org.sonar.ce.task.projectanalysis.step.SendIssueNotificationsStep.NOTIF_TYPES;
  99. import static org.sonar.db.component.BranchType.BRANCH;
  100. import static org.sonar.db.component.BranchType.PULL_REQUEST;
  101. import static org.sonar.db.component.ComponentTesting.newBranchDto;
  102. import static org.sonar.db.component.ComponentTesting.newFileDto;
  103. import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
  104. import static org.sonar.db.component.ComponentTesting.newBranchComponent;
  105. import static org.sonar.db.issue.IssueTesting.newIssue;
  106. import static org.sonar.db.rule.RuleTesting.newRule;
  107. public class SendIssueNotificationsStepTest extends BaseStepTest {
  108. private static final String BRANCH_NAME = "feature";
  109. private static final String PULL_REQUEST_ID = "pr-123";
  110. private static final long ANALYSE_DATE = 123L;
  111. private static final int FIVE_MINUTES_IN_MS = 1000 * 60 * 5;
  112. private static final Duration ISSUE_DURATION = Duration.create(100L);
  113. private static final Component FILE = builder(Type.FILE, 11).build();
  114. private static final Component PROJECT = builder(Type.PROJECT, 1)
  115. .setProjectVersion(randomAlphanumeric(10))
  116. .addChildren(FILE).build();
  117. @Rule
  118. public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule()
  119. .setRoot(PROJECT);
  120. @Rule
  121. public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule()
  122. .setBranch(new DefaultBranchImpl())
  123. .setAnalysisDate(new Date(ANALYSE_DATE));
  124. @Rule
  125. public TemporaryFolder temp = new TemporaryFolder();
  126. @Rule
  127. public DbTester db = DbTester.create(System2.INSTANCE);
  128. private final Random random = new Random();
  129. private final RuleType[] RULE_TYPES_EXCEPT_HOTSPOTS = Stream.of(RuleType.values()).filter(r -> r != SECURITY_HOTSPOT).toArray(RuleType[]::new);
  130. private final RuleType randomRuleType = RULE_TYPES_EXCEPT_HOTSPOTS[random.nextInt(RULE_TYPES_EXCEPT_HOTSPOTS.length)];
  131. @SuppressWarnings("unchecked")
  132. private Class<Map<String, UserDto>> assigneeCacheType = (Class<Map<String, UserDto>>) (Object) Map.class;
  133. @SuppressWarnings("unchecked")
  134. private Class<Set<DefaultIssue>> setType = (Class<Set<DefaultIssue>>) (Class<?>) Set.class;
  135. @SuppressWarnings("unchecked")
  136. private Class<Map<String, UserDto>> mapType = (Class<Map<String, UserDto>>) (Class<?>) Map.class;
  137. private ArgumentCaptor<Map<String, UserDto>> assigneeCacheCaptor = ArgumentCaptor.forClass(assigneeCacheType);
  138. private ArgumentCaptor<Set<DefaultIssue>> issuesSetCaptor = forClass(setType);
  139. private ArgumentCaptor<Map<String, UserDto>> assigneeByUuidCaptor = forClass(mapType);
  140. private NotificationService notificationService = mock(NotificationService.class);
  141. private NotificationFactory notificationFactory = mock(NotificationFactory.class);
  142. private NewIssuesNotification newIssuesNotificationMock = createNewIssuesNotificationMock();
  143. private MyNewIssuesNotification myNewIssuesNotificationMock = createMyNewIssuesNotificationMock();
  144. private ProtoIssueCache protoIssueCache;
  145. private SendIssueNotificationsStep underTest;
  146. @Before
  147. public void setUp() throws Exception {
  148. protoIssueCache = new ProtoIssueCache(temp.newFile(), System2.INSTANCE);
  149. underTest = new SendIssueNotificationsStep(protoIssueCache, treeRootHolder, notificationService, analysisMetadataHolder,
  150. notificationFactory, db.getDbClient());
  151. when(notificationFactory.newNewIssuesNotification(any(assigneeCacheType))).thenReturn(newIssuesNotificationMock);
  152. when(notificationFactory.newMyNewIssuesNotification(any(assigneeCacheType))).thenReturn(myNewIssuesNotificationMock);
  153. }
  154. @Test
  155. public void do_not_send_notifications_if_no_subscribers() {
  156. analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
  157. when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(false);
  158. TestComputationStepContext context = new TestComputationStepContext();
  159. underTest.execute(context);
  160. verify(notificationService, never()).deliver(any(Notification.class));
  161. verify(notificationService, never()).deliverEmails(anyCollection());
  162. verifyStatistics(context, 0, 0, 0);
  163. }
  164. @Test
  165. public void send_global_new_issues_notification() {
  166. analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
  167. protoIssueCache.newAppender().append(
  168. createIssue().setType(randomRuleType).setEffort(ISSUE_DURATION)
  169. .setCreationDate(new Date(ANALYSE_DATE)))
  170. .close();
  171. when(notificationService.hasProjectSubscribersForTypes(eq(PROJECT.getUuid()), any())).thenReturn(true);
  172. TestComputationStepContext context = new TestComputationStepContext();
  173. underTest.execute(context);
  174. verify(notificationService).deliver(newIssuesNotificationMock);
  175. verify(newIssuesNotificationMock).setProject(PROJECT.getKey(), PROJECT.getName(), null, null);
  176. verify(newIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
  177. verify(newIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), any());
  178. verify(newIssuesNotificationMock).setDebt(ISSUE_DURATION);
  179. verifyStatistics(context, 1, 0, 0);
  180. }
  181. @Test
  182. public void send_global_new_issues_notification_only_for_non_backdated_issues() {
  183. Random random = new Random();
  184. Integer[] efforts = IntStream.range(0, 1 + random.nextInt(10)).mapToObj(i -> 10_000 * i).toArray(Integer[]::new);
  185. Integer[] backDatedEfforts = IntStream.range(0, 1 + random.nextInt(10)).mapToObj(i -> 10 + random.nextInt(100)).toArray(Integer[]::new);
  186. Duration expectedEffort = Duration.create(stream(efforts).mapToInt(i -> i).sum());
  187. List<DefaultIssue> issues = concat(stream(efforts)
  188. .map(effort -> createIssue().setType(randomRuleType).setEffort(Duration.create(effort))
  189. .setCreationDate(new Date(ANALYSE_DATE))),
  190. stream(backDatedEfforts)
  191. .map(effort -> createIssue().setType(randomRuleType).setEffort(Duration.create(effort))
  192. .setCreationDate(new Date(ANALYSE_DATE - FIVE_MINUTES_IN_MS))))
  193. .collect(toList());
  194. shuffle(issues);
  195. DiskCache.CacheAppender issueCache = this.protoIssueCache.newAppender();
  196. issues.forEach(issueCache::append);
  197. issueCache.close();
  198. analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
  199. when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
  200. TestComputationStepContext context = new TestComputationStepContext();
  201. underTest.execute(context);
  202. verify(notificationService).deliver(newIssuesNotificationMock);
  203. ArgumentCaptor<NewIssuesStatistics.Stats> statsCaptor = forClass(NewIssuesStatistics.Stats.class);
  204. verify(newIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), statsCaptor.capture());
  205. verify(newIssuesNotificationMock).setDebt(expectedEffort);
  206. NewIssuesStatistics.Stats stats = statsCaptor.getValue();
  207. assertThat(stats.hasIssues()).isTrue();
  208. // just checking all issues have been added to the stats
  209. DistributedMetricStatsInt severity = stats.getDistributedMetricStats(NewIssuesStatistics.Metric.RULE_TYPE);
  210. assertThat(severity.getOnCurrentAnalysis()).isEqualTo(efforts.length);
  211. assertThat(severity.getTotal()).isEqualTo(backDatedEfforts.length + efforts.length);
  212. verifyStatistics(context, 1, 0, 0);
  213. }
  214. @Test
  215. public void do_not_send_global_new_issues_notification_if_issue_has_been_backdated() {
  216. analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
  217. protoIssueCache.newAppender().append(
  218. createIssue().setType(randomRuleType).setEffort(ISSUE_DURATION)
  219. .setCreationDate(new Date(ANALYSE_DATE - FIVE_MINUTES_IN_MS)))
  220. .close();
  221. when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
  222. TestComputationStepContext context = new TestComputationStepContext();
  223. underTest.execute(context);
  224. verify(notificationService, never()).deliver(any(Notification.class));
  225. verify(notificationService, never()).deliverEmails(anyCollection());
  226. verifyStatistics(context, 0, 0, 0);
  227. }
  228. @Test
  229. public void send_global_new_issues_notification_on_branch() {
  230. ComponentDto project = newPrivateProjectDto();
  231. ComponentDto branch = setUpBranch(project, BRANCH);
  232. protoIssueCache.newAppender().append(
  233. createIssue().setType(randomRuleType).setEffort(ISSUE_DURATION).setCreationDate(new Date(ANALYSE_DATE))).close();
  234. when(notificationService.hasProjectSubscribersForTypes(branch.uuid(), NOTIF_TYPES)).thenReturn(true);
  235. analysisMetadataHolder.setProject(Project.from(project));
  236. analysisMetadataHolder.setBranch(newBranch(BranchType.BRANCH));
  237. TestComputationStepContext context = new TestComputationStepContext();
  238. underTest.execute(context);
  239. verify(notificationService).deliver(newIssuesNotificationMock);
  240. verify(newIssuesNotificationMock).setProject(branch.getKey(), branch.longName(), BRANCH_NAME, null);
  241. verify(newIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
  242. verify(newIssuesNotificationMock).setStatistics(eq(branch.longName()), any(NewIssuesStatistics.Stats.class));
  243. verify(newIssuesNotificationMock).setDebt(ISSUE_DURATION);
  244. verifyStatistics(context, 1, 0, 0);
  245. }
  246. @Test
  247. public void do_not_send_global_new_issues_notification_on_pull_request() {
  248. ComponentDto project = newPrivateProjectDto();
  249. ComponentDto branch = setUpBranch(project, PULL_REQUEST);
  250. protoIssueCache.newAppender().append(
  251. createIssue().setType(randomRuleType).setEffort(ISSUE_DURATION).setCreationDate(new Date(ANALYSE_DATE))).close();
  252. when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true);
  253. analysisMetadataHolder.setProject(Project.from(project));
  254. analysisMetadataHolder.setBranch(newPullRequest());
  255. analysisMetadataHolder.setPullRequestKey(PULL_REQUEST_ID);
  256. TestComputationStepContext context = new TestComputationStepContext();
  257. underTest.execute(context);
  258. verifyZeroInteractions(notificationService, newIssuesNotificationMock);
  259. }
  260. private DefaultIssue createIssue() {
  261. return new DefaultIssue().setKey("k").setProjectKey("p").setStatus("OPEN").setProjectUuid("uuid").setComponentKey("c").setRuleKey(RuleKey.of("r", "r"));
  262. }
  263. @Test
  264. public void do_not_send_global_new_issues_notification_on_branch_if_issue_has_been_backdated() {
  265. ComponentDto project = newPrivateProjectDto();
  266. ComponentDto branch = setUpBranch(project, BRANCH);
  267. protoIssueCache.newAppender().append(
  268. createIssue().setType(randomRuleType).setEffort(ISSUE_DURATION).setCreationDate(new Date(ANALYSE_DATE - FIVE_MINUTES_IN_MS))).close();
  269. when(notificationService.hasProjectSubscribersForTypes(branch.uuid(), NOTIF_TYPES)).thenReturn(true);
  270. analysisMetadataHolder.setProject(Project.from(project));
  271. analysisMetadataHolder.setBranch(newBranch(BranchType.BRANCH));
  272. TestComputationStepContext context = new TestComputationStepContext();
  273. underTest.execute(context);
  274. verify(notificationService, never()).deliver(any(Notification.class));
  275. verify(notificationService, never()).deliverEmails(anyCollection());
  276. verifyStatistics(context, 0, 0, 0);
  277. }
  278. @Test
  279. public void send_new_issues_notification_to_user() {
  280. UserDto user = db.users().insertUser();
  281. analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
  282. protoIssueCache.newAppender().append(
  283. createIssue().setType(randomRuleType).setEffort(ISSUE_DURATION).setAssigneeUuid(user.getUuid()).setCreationDate(new Date(ANALYSE_DATE)))
  284. .close();
  285. when(notificationService.hasProjectSubscribersForTypes(eq(PROJECT.getUuid()), any())).thenReturn(true);
  286. TestComputationStepContext context = new TestComputationStepContext();
  287. underTest.execute(context);
  288. verify(notificationService).deliverEmails(ImmutableSet.of(newIssuesNotificationMock));
  289. verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock));
  290. // old API compatibility call
  291. verify(notificationService).deliver(newIssuesNotificationMock);
  292. verify(notificationService).deliver(myNewIssuesNotificationMock);
  293. verify(myNewIssuesNotificationMock).setAssignee(any(UserDto.class));
  294. verify(myNewIssuesNotificationMock).setProject(PROJECT.getKey(), PROJECT.getName(), null, null);
  295. verify(myNewIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
  296. verify(myNewIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), any(NewIssuesStatistics.Stats.class));
  297. verify(myNewIssuesNotificationMock).setDebt(ISSUE_DURATION);
  298. verifyStatistics(context, 1, 1, 0);
  299. }
  300. @Test
  301. public void send_new_issues_notification_to_user_only_for_those_assigned_to_her() throws IOException {
  302. UserDto perceval = db.users().insertUser(u -> u.setLogin("perceval"));
  303. Integer[] assigned = IntStream.range(0, 5).mapToObj(i -> 10_000 * i).toArray(Integer[]::new);
  304. Duration expectedEffort = Duration.create(stream(assigned).mapToInt(i -> i).sum());
  305. UserDto arthur = db.users().insertUser(u -> u.setLogin("arthur"));
  306. Integer[] assignedToOther = IntStream.range(0, 3).mapToObj(i -> 10).toArray(Integer[]::new);
  307. List<DefaultIssue> issues = concat(stream(assigned)
  308. .map(effort -> createIssue().setType(randomRuleType).setEffort(Duration.create(effort))
  309. .setAssigneeUuid(perceval.getUuid())
  310. .setNew(true)
  311. .setCreationDate(new Date(ANALYSE_DATE))),
  312. stream(assignedToOther)
  313. .map(effort -> createIssue().setType(randomRuleType).setEffort(Duration.create(effort))
  314. .setAssigneeUuid(arthur.getUuid())
  315. .setNew(true)
  316. .setCreationDate(new Date(ANALYSE_DATE))))
  317. .collect(toList());
  318. shuffle(issues);
  319. ProtoIssueCache protoIssueCache = new ProtoIssueCache(temp.newFile(), System2.INSTANCE);
  320. DiskCache.CacheAppender newIssueCache = protoIssueCache.newAppender();
  321. issues.forEach(newIssueCache::append);
  322. newIssueCache.close();
  323. analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
  324. when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
  325. NotificationFactory notificationFactory = mock(NotificationFactory.class);
  326. NewIssuesNotification newIssuesNotificationMock = createNewIssuesNotificationMock();
  327. when(notificationFactory.newNewIssuesNotification(assigneeCacheCaptor.capture()))
  328. .thenReturn(newIssuesNotificationMock);
  329. MyNewIssuesNotification myNewIssuesNotificationMock1 = createMyNewIssuesNotificationMock();
  330. MyNewIssuesNotification myNewIssuesNotificationMock2 = createMyNewIssuesNotificationMock();
  331. doReturn(myNewIssuesNotificationMock1).doReturn(myNewIssuesNotificationMock2).when(notificationFactory).newMyNewIssuesNotification(any(assigneeCacheType));
  332. TestComputationStepContext context = new TestComputationStepContext();
  333. new SendIssueNotificationsStep(protoIssueCache, treeRootHolder, notificationService, analysisMetadataHolder, notificationFactory, db.getDbClient())
  334. .execute(context);
  335. verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock1, myNewIssuesNotificationMock2));
  336. // old API compatibility
  337. verify(notificationService).deliver(myNewIssuesNotificationMock1);
  338. verify(notificationService).deliver(myNewIssuesNotificationMock2);
  339. verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
  340. verify(notificationFactory, times(2)).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
  341. verifyNoMoreInteractions(notificationFactory);
  342. verifyAssigneeCache(assigneeCacheCaptor, perceval, arthur);
  343. Map<String, MyNewIssuesNotification> myNewIssuesNotificationMocksByUsersName = new HashMap<>();
  344. ArgumentCaptor<UserDto> userCaptor1 = forClass(UserDto.class);
  345. verify(myNewIssuesNotificationMock1).setAssignee(userCaptor1.capture());
  346. myNewIssuesNotificationMocksByUsersName.put(userCaptor1.getValue().getLogin(), myNewIssuesNotificationMock1);
  347. ArgumentCaptor<UserDto> userCaptor2 = forClass(UserDto.class);
  348. verify(myNewIssuesNotificationMock2).setAssignee(userCaptor2.capture());
  349. myNewIssuesNotificationMocksByUsersName.put(userCaptor2.getValue().getLogin(), myNewIssuesNotificationMock2);
  350. MyNewIssuesNotification myNewIssuesNotificationMock = myNewIssuesNotificationMocksByUsersName.get("perceval");
  351. ArgumentCaptor<NewIssuesStatistics.Stats> statsCaptor = forClass(NewIssuesStatistics.Stats.class);
  352. verify(myNewIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), statsCaptor.capture());
  353. verify(myNewIssuesNotificationMock).setDebt(expectedEffort);
  354. NewIssuesStatistics.Stats stats = statsCaptor.getValue();
  355. assertThat(stats.hasIssues()).isTrue();
  356. // just checking all issues have been added to the stats
  357. DistributedMetricStatsInt severity = stats.getDistributedMetricStats(NewIssuesStatistics.Metric.RULE_TYPE);
  358. assertThat(severity.getOnCurrentAnalysis()).isEqualTo(assigned.length);
  359. assertThat(severity.getTotal()).isEqualTo(assigned.length);
  360. verifyStatistics(context, 1, 2, 0);
  361. }
  362. @Test
  363. public void send_new_issues_notification_to_user_only_for_non_backdated_issues() {
  364. UserDto user = db.users().insertUser();
  365. Random random = new Random();
  366. Integer[] efforts = IntStream.range(0, 1 + random.nextInt(10)).mapToObj(i -> 10_000 * i).toArray(Integer[]::new);
  367. Integer[] backDatedEfforts = IntStream.range(0, 1 + random.nextInt(10)).mapToObj(i -> 10 + random.nextInt(100)).toArray(Integer[]::new);
  368. Duration expectedEffort = Duration.create(stream(efforts).mapToInt(i -> i).sum());
  369. List<DefaultIssue> issues = concat(stream(efforts)
  370. .map(effort -> createIssue().setType(randomRuleType).setEffort(Duration.create(effort))
  371. .setAssigneeUuid(user.getUuid())
  372. .setCreationDate(new Date(ANALYSE_DATE))),
  373. stream(backDatedEfforts)
  374. .map(effort -> createIssue().setType(randomRuleType).setEffort(Duration.create(effort))
  375. .setAssigneeUuid(user.getUuid())
  376. .setCreationDate(new Date(ANALYSE_DATE - FIVE_MINUTES_IN_MS))))
  377. .collect(toList());
  378. shuffle(issues);
  379. DiskCache.CacheAppender issueCache = this.protoIssueCache.newAppender();
  380. issues.forEach(issueCache::append);
  381. issueCache.close();
  382. analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
  383. when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
  384. TestComputationStepContext context = new TestComputationStepContext();
  385. underTest.execute(context);
  386. verify(notificationService).deliver(newIssuesNotificationMock);
  387. verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock));
  388. // old API compatibility
  389. verify(notificationService).deliver(myNewIssuesNotificationMock);
  390. verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
  391. verify(notificationFactory).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
  392. verifyNoMoreInteractions(notificationFactory);
  393. verifyAssigneeCache(assigneeCacheCaptor, user);
  394. verify(myNewIssuesNotificationMock).setAssignee(any(UserDto.class));
  395. ArgumentCaptor<NewIssuesStatistics.Stats> statsCaptor = forClass(NewIssuesStatistics.Stats.class);
  396. verify(myNewIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), statsCaptor.capture());
  397. verify(myNewIssuesNotificationMock).setDebt(expectedEffort);
  398. NewIssuesStatistics.Stats stats = statsCaptor.getValue();
  399. assertThat(stats.hasIssues()).isTrue();
  400. // just checking all issues have been added to the stats
  401. DistributedMetricStatsInt severity = stats.getDistributedMetricStats(NewIssuesStatistics.Metric.RULE_TYPE);
  402. assertThat(severity.getOnCurrentAnalysis()).isEqualTo(efforts.length);
  403. assertThat(severity.getTotal()).isEqualTo(backDatedEfforts.length + efforts.length);
  404. verifyStatistics(context, 1, 1, 0);
  405. }
  406. private static void verifyAssigneeCache(ArgumentCaptor<Map<String, UserDto>> assigneeCacheCaptor, UserDto... users) {
  407. Map<String, UserDto> cache = assigneeCacheCaptor.getAllValues().iterator().next();
  408. assertThat(assigneeCacheCaptor.getAllValues())
  409. .filteredOn(t -> t != cache)
  410. .isEmpty();
  411. Tuple[] expected = stream(users).map(user -> tuple(user.getUuid(), user.getUuid(), user.getUuid(), user.getLogin())).toArray(Tuple[]::new);
  412. assertThat(cache.entrySet())
  413. .extracting(Map.Entry::getKey, t -> t.getValue().getUuid(), t -> t.getValue().getUuid(), t -> t.getValue().getLogin())
  414. .containsOnly(expected);
  415. }
  416. @Test
  417. public void do_not_send_new_issues_notification_to_user_if_issue_is_backdated() {
  418. analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
  419. UserDto user = db.users().insertUser();
  420. protoIssueCache.newAppender().append(
  421. createIssue().setType(randomRuleType).setEffort(ISSUE_DURATION).setAssigneeUuid(user.getUuid())
  422. .setCreationDate(new Date(ANALYSE_DATE - FIVE_MINUTES_IN_MS)))
  423. .close();
  424. when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
  425. TestComputationStepContext context = new TestComputationStepContext();
  426. underTest.execute(context);
  427. verify(notificationService, never()).deliver(any(Notification.class));
  428. verify(notificationService, never()).deliverEmails(anyCollection());
  429. verifyStatistics(context, 0, 0, 0);
  430. }
  431. @Test
  432. public void send_issues_change_notification() {
  433. sendIssueChangeNotification(ANALYSE_DATE);
  434. }
  435. @Test
  436. public void do_not_send_new_issues_notifications_for_hotspot() {
  437. UserDto user = db.users().insertUser();
  438. ComponentDto project = newPrivateProjectDto().setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName());
  439. ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName());
  440. RuleDefinitionDto ruleDefinitionDto = newRule();
  441. prepareIssue(ANALYSE_DATE, user, project, file, ruleDefinitionDto, RuleType.SECURITY_HOTSPOT);
  442. analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
  443. when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
  444. TestComputationStepContext context = new TestComputationStepContext();
  445. underTest.execute(context);
  446. verify(notificationService, never()).deliver(any(Notification.class));
  447. verify(notificationService, never()).deliverEmails(anyCollection());
  448. verifyStatistics(context, 0, 0, 0);
  449. }
  450. @Test
  451. public void send_issues_change_notification_even_if_issue_is_backdated() {
  452. sendIssueChangeNotification(ANALYSE_DATE - FIVE_MINUTES_IN_MS);
  453. }
  454. private void sendIssueChangeNotification(long issueCreatedAt) {
  455. UserDto user = db.users().insertUser();
  456. ComponentDto project = newPrivateProjectDto().setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName());
  457. analysisMetadataHolder.setProject(Project.from(project));
  458. ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName());
  459. treeRootHolder.setRoot(builder(Type.PROJECT, 2).setKey(project.getDbKey()).setPublicKey(project.getKey()).setName(project.longName()).setUuid(project.uuid())
  460. .addChildren(
  461. builder(Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).build())
  462. .build());
  463. RuleDefinitionDto ruleDefinitionDto = newRule();
  464. RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)];
  465. DefaultIssue issue = prepareIssue(issueCreatedAt, user, project, file, ruleDefinitionDto, randomTypeExceptHotspot);
  466. IssuesChangesNotification issuesChangesNotification = mock(IssuesChangesNotification.class);
  467. when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true);
  468. when(notificationFactory.newIssuesChangesNotification(anySet(), anyMap())).thenReturn(issuesChangesNotification);
  469. underTest.execute(new TestComputationStepContext());
  470. verify(notificationFactory).newIssuesChangesNotification(issuesSetCaptor.capture(), assigneeByUuidCaptor.capture());
  471. assertThat(issuesSetCaptor.getValue()).hasSize(1);
  472. assertThat(issuesSetCaptor.getValue().iterator().next()).isEqualTo(issue);
  473. assertThat(assigneeByUuidCaptor.getValue()).hasSize(1);
  474. assertThat(assigneeByUuidCaptor.getValue().get(user.getUuid())).isNotNull();
  475. verify(notificationService).hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES);
  476. verify(notificationService).deliverEmails(singleton(issuesChangesNotification));
  477. verify(notificationService).deliver(issuesChangesNotification);
  478. verifyNoMoreInteractions(notificationService);
  479. }
  480. private DefaultIssue prepareIssue(long issueCreatedAt, UserDto user, ComponentDto project, ComponentDto file, RuleDefinitionDto ruleDefinitionDto, RuleType type) {
  481. DefaultIssue issue = newIssue(ruleDefinitionDto, project, file).setType(type).toDefaultIssue()
  482. .setNew(false).setChanged(true).setSendNotifications(true).setCreationDate(new Date(issueCreatedAt)).setAssigneeUuid(user.getUuid());
  483. protoIssueCache.newAppender().append(issue).close();
  484. when(notificationService.hasProjectSubscribersForTypes(project.projectUuid(), NOTIF_TYPES)).thenReturn(true);
  485. return issue;
  486. }
  487. @Test
  488. public void send_issues_change_notification_on_branch() {
  489. sendIssueChangeNotificationOnBranch(ANALYSE_DATE);
  490. }
  491. @Test
  492. public void send_issues_change_notification_on_branch_even_if_issue_is_backdated() {
  493. sendIssueChangeNotificationOnBranch(ANALYSE_DATE - FIVE_MINUTES_IN_MS);
  494. }
  495. private void sendIssueChangeNotificationOnBranch(long issueCreatedAt) {
  496. ComponentDto project = newPrivateProjectDto();
  497. ComponentDto branch = newBranchComponent(project, newBranchDto(project).setKey(BRANCH_NAME));
  498. ComponentDto file = newFileDto(branch);
  499. treeRootHolder.setRoot(builder(Type.PROJECT, 2).setKey(branch.getDbKey()).setPublicKey(branch.getKey()).setName(branch.longName()).setUuid(branch.uuid()).addChildren(
  500. builder(Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).build()).build());
  501. analysisMetadataHolder.setProject(Project.from(project));
  502. RuleDefinitionDto ruleDefinitionDto = newRule();
  503. RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)];
  504. DefaultIssue issue = newIssue(ruleDefinitionDto, branch, file).setType(randomTypeExceptHotspot).toDefaultIssue()
  505. .setNew(false)
  506. .setChanged(true)
  507. .setSendNotifications(true)
  508. .setCreationDate(new Date(issueCreatedAt));
  509. protoIssueCache.newAppender().append(issue).close();
  510. when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true);
  511. IssuesChangesNotification issuesChangesNotification = mock(IssuesChangesNotification.class);
  512. when(notificationFactory.newIssuesChangesNotification(anySet(), anyMap())).thenReturn(issuesChangesNotification);
  513. analysisMetadataHolder.setBranch(newBranch(BranchType.BRANCH));
  514. underTest.execute(new TestComputationStepContext());
  515. verify(notificationFactory).newIssuesChangesNotification(issuesSetCaptor.capture(), assigneeByUuidCaptor.capture());
  516. assertThat(issuesSetCaptor.getValue()).hasSize(1);
  517. assertThat(issuesSetCaptor.getValue().iterator().next()).isEqualTo(issue);
  518. assertThat(assigneeByUuidCaptor.getValue()).isEmpty();
  519. verify(notificationService).hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES);
  520. verify(notificationService).deliverEmails(singleton(issuesChangesNotification));
  521. verify(notificationService).deliver(issuesChangesNotification);
  522. verifyNoMoreInteractions(notificationService);
  523. }
  524. @Test
  525. public void sends_one_issue_change_notification_every_1000_issues() {
  526. UserDto user = db.users().insertUser();
  527. ComponentDto project = newPrivateProjectDto().setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName());
  528. ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName());
  529. RuleDefinitionDto ruleDefinitionDto = newRule();
  530. RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)];
  531. List<DefaultIssue> issues = IntStream.range(0, 2001 + new Random().nextInt(10))
  532. .mapToObj(i -> newIssue(ruleDefinitionDto, project, file).setKee("uuid_" + i).setType(randomTypeExceptHotspot).toDefaultIssue()
  533. .setNew(false).setChanged(true).setSendNotifications(true).setAssigneeUuid(user.getUuid()))
  534. .collect(toList());
  535. DiskCache.CacheAppender cacheAppender = protoIssueCache.newAppender();
  536. issues.forEach(cacheAppender::append);
  537. cacheAppender.close();
  538. analysisMetadataHolder.setProject(Project.from(project));
  539. NewIssuesFactoryCaptor newIssuesFactoryCaptor = new NewIssuesFactoryCaptor(() -> mock(IssuesChangesNotification.class));
  540. when(notificationFactory.newIssuesChangesNotification(anySet(), anyMap())).thenAnswer(newIssuesFactoryCaptor);
  541. when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
  542. when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true);
  543. underTest.execute(new TestComputationStepContext());
  544. verify(notificationFactory, times(3)).newIssuesChangesNotification(anySet(), anyMap());
  545. assertThat(newIssuesFactoryCaptor.issuesSetCaptor).hasSize(3);
  546. assertThat(newIssuesFactoryCaptor.issuesSetCaptor.get(0)).hasSize(1000);
  547. assertThat(newIssuesFactoryCaptor.issuesSetCaptor.get(1)).hasSize(1000);
  548. assertThat(newIssuesFactoryCaptor.issuesSetCaptor.get(2)).hasSize(issues.size() - 2000);
  549. assertThat(newIssuesFactoryCaptor.assigneeCacheCaptor).hasSize(3);
  550. assertThat(newIssuesFactoryCaptor.assigneeCacheCaptor).containsOnly(newIssuesFactoryCaptor.assigneeCacheCaptor.iterator().next());
  551. ArgumentCaptor<Collection> collectionCaptor = forClass(Collection.class);
  552. verify(notificationService, times(3)).deliverEmails(collectionCaptor.capture());
  553. assertThat(collectionCaptor.getAllValues()).hasSize(3);
  554. assertThat(collectionCaptor.getAllValues().get(0)).hasSize(1);
  555. assertThat(collectionCaptor.getAllValues().get(1)).hasSize(1);
  556. assertThat(collectionCaptor.getAllValues().get(2)).hasSize(1);
  557. verify(notificationService, times(3)).deliver(any(IssuesChangesNotification.class));
  558. }
  559. /**
  560. * Since the very same Set object is passed to {@link NotificationFactory#newIssuesChangesNotification(Set, Map)} and
  561. * reset between each call. We must make a copy of each argument to capture what's been passed to the factory.
  562. * This is of course not supported by Mockito's {@link ArgumentCaptor} and we implement this ourselves with a
  563. * {@link Answer}.
  564. */
  565. private static class NewIssuesFactoryCaptor implements Answer<Object> {
  566. private final Supplier<IssuesChangesNotification> delegate;
  567. private final List<Set<DefaultIssue>> issuesSetCaptor = new ArrayList<>();
  568. private final List<Map<String, UserDto>> assigneeCacheCaptor = new ArrayList<>();
  569. private NewIssuesFactoryCaptor(Supplier<IssuesChangesNotification> delegate) {
  570. this.delegate = delegate;
  571. }
  572. @Override
  573. public Object answer(InvocationOnMock t) {
  574. Set<DefaultIssue> issuesSet = t.getArgument(0);
  575. Map<String, UserDto> assigneeCatch = t.getArgument(1);
  576. issuesSetCaptor.add(ImmutableSet.copyOf(issuesSet));
  577. assigneeCacheCaptor.add(ImmutableMap.copyOf(assigneeCatch));
  578. return delegate.get();
  579. }
  580. }
  581. private NewIssuesNotification createNewIssuesNotificationMock() {
  582. NewIssuesNotification notification = mock(NewIssuesNotification.class);
  583. when(notification.setProject(any(), any(), any(), any())).thenReturn(notification);
  584. when(notification.setProjectVersion(any())).thenReturn(notification);
  585. when(notification.setAnalysisDate(any())).thenReturn(notification);
  586. when(notification.setStatistics(any(), any())).thenReturn(notification);
  587. when(notification.setDebt(any())).thenReturn(notification);
  588. return notification;
  589. }
  590. private MyNewIssuesNotification createMyNewIssuesNotificationMock() {
  591. MyNewIssuesNotification notification = mock(MyNewIssuesNotification.class);
  592. when(notification.setAssignee(any(UserDto.class))).thenReturn(notification);
  593. when(notification.setProject(any(), any(), any(), any())).thenReturn(notification);
  594. when(notification.setProjectVersion(any())).thenReturn(notification);
  595. when(notification.setAnalysisDate(any())).thenReturn(notification);
  596. when(notification.setStatistics(any(), any())).thenReturn(notification);
  597. when(notification.setDebt(any())).thenReturn(notification);
  598. return notification;
  599. }
  600. private static Branch newBranch(BranchType type) {
  601. Branch branch = mock(Branch.class);
  602. when(branch.isMain()).thenReturn(false);
  603. when(branch.getName()).thenReturn(BRANCH_NAME);
  604. when(branch.getType()).thenReturn(type);
  605. return branch;
  606. }
  607. private static Branch newPullRequest() {
  608. Branch branch = mock(Branch.class);
  609. when(branch.isMain()).thenReturn(false);
  610. when(branch.getType()).thenReturn(PULL_REQUEST);
  611. when(branch.getName()).thenReturn(BRANCH_NAME);
  612. when(branch.getPullRequestKey()).thenReturn(PULL_REQUEST_ID);
  613. return branch;
  614. }
  615. private ComponentDto setUpBranch(ComponentDto project, BranchType branchType) {
  616. ComponentDto branch = newBranchComponent(project, newBranchDto(project, branchType).setKey(BRANCH_NAME));
  617. ComponentDto file = newFileDto(branch);
  618. treeRootHolder.setRoot(builder(Type.PROJECT, 2).setKey(branch.getDbKey()).setPublicKey(branch.getKey()).setName(branch.longName()).setUuid(branch.uuid()).addChildren(
  619. builder(Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).build()).build());
  620. return branch;
  621. }
  622. private static void verifyStatistics(TestComputationStepContext context, int expectedNewIssuesNotifications, int expectedMyNewIssuesNotifications,
  623. int expectedIssueChangesNotifications) {
  624. context.getStatistics().assertValue("newIssuesNotifs", expectedNewIssuesNotifications);
  625. context.getStatistics().assertValue("myNewIssuesNotifs", expectedMyNewIssuesNotifications);
  626. context.getStatistics().assertValue("changesNotifs", expectedIssueChangesNotifications);
  627. }
  628. @Override
  629. protected ComputationStep step() {
  630. return underTest;
  631. }
  632. }