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