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