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