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