3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 package org.sonar.ce.task.projectanalysis.step;
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;
31 import java.util.Random;
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;
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;
112 public class SendIssueNotificationsStepIT extends BaseStepTest {
114 private static final String BRANCH_NAME = "feature";
115 private static final String PULL_REQUEST_ID = "pr-123";
117 private static final long ANALYSE_DATE = 123L;
118 private static final int FIVE_MINUTES_IN_MS = 1000 * 60 * 5;
120 private static final Duration ISSUE_DURATION = Duration.create(100L);
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();
128 public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule()
131 public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule()
132 .setBranch(new DefaultBranchImpl(DEFAULT_MAIN_BRANCH_NAME))
133 .setAnalysisDate(new Date(ANALYSE_DATE));
135 public TemporaryFolder temp = new TemporaryFolder();
137 public DbTester db = DbTester.create(System2.INSTANCE);
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();
156 private ProtoIssueCache protoIssueCache;
157 private SendIssueNotificationsStep underTest;
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);
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);
173 TestComputationStepContext context = new TestComputationStepContext();
174 underTest.execute(context);
176 verify(notificationService, never()).deliver(any(Notification.class));
177 verify(notificationService, never()).deliverEmails(anyCollection());
178 verifyStatistics(context, 0, 0, 0);
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)))
188 when(notificationService.hasProjectSubscribersForTypes(eq(PROJECT.getUuid()), any())).thenReturn(true);
190 TestComputationStepContext context = new TestComputationStepContext();
191 underTest.execute(context);
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);
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))))
215 DiskCache.CacheAppender issueCache = this.protoIssueCache.newAppender();
216 issues.forEach(issueCache::append);
218 analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
219 when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
221 TestComputationStepContext context = new TestComputationStepContext();
222 underTest.execute(context);
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);
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)))
244 when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
246 TestComputationStepContext context = new TestComputationStepContext();
247 underTest.execute(context);
249 verify(notificationService, never()).deliver(any(Notification.class));
250 verify(notificationService, never()).deliverEmails(anyCollection());
251 verifyStatistics(context, 0, 0, 0);
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));
264 TestComputationStepContext context = new TestComputationStepContext();
265 underTest.execute(context);
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);
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);
286 TestComputationStepContext context = new TestComputationStepContext();
287 underTest.execute(context);
289 verifyNoInteractions(notificationService, newIssuesNotificationMock);
292 private DefaultIssue createIssue() {
293 return new DefaultIssue().setKey("k").setProjectKey("p").setStatus("OPEN").setProjectUuid("uuid").setComponentKey("c").setRuleKey(RuleKey.of("r", "r"));
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));
306 TestComputationStepContext context = new TestComputationStepContext();
307 underTest.execute(context);
309 verify(notificationService, never()).deliver(any(Notification.class));
310 verify(notificationService, never()).deliverEmails(anyCollection());
311 verifyStatistics(context, 0, 0, 0);
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()));
319 protoIssueCache.newAppender().append(
320 createIssue().setType(randomRuleType).setEffort(ISSUE_DURATION).setAssigneeUuid(user.getUuid()).setCreationDate(new Date(ANALYSE_DATE)))
322 when(notificationService.hasProjectSubscribersForTypes(eq(PROJECT.getUuid()), any())).thenReturn(true);
324 TestComputationStepContext context = new TestComputationStepContext();
325 underTest.execute(context);
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);
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());
346 UserDto arthur = db.users().insertUser(u -> u.setLogin("arthur"));
347 Integer[] assignedToOther = IntStream.range(0, 3).mapToObj(i -> 10).toArray(Integer[]::new);
349 List<DefaultIssue> issues = concat(stream(assigned)
350 .map(effort -> createIssue().setType(randomRuleType).setEffort(Duration.create(effort))
351 .setAssigneeUuid(perceval.getUuid())
353 .setCreationDate(new Date(ANALYSE_DATE))),
354 stream(assignedToOther)
355 .map(effort -> createIssue().setType(randomRuleType).setEffort(Duration.create(effort))
356 .setAssigneeUuid(arthur.getUuid())
358 .setCreationDate(new Date(ANALYSE_DATE))))
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);
368 NotificationFactory notificationFactory = mock(NotificationFactory.class);
369 NewIssuesNotification newIssuesNotificationMock = createNewIssuesNotificationMock();
370 when(notificationFactory.newNewIssuesNotification(assigneeCacheCaptor.capture()))
371 .thenReturn(newIssuesNotificationMock);
373 MyNewIssuesNotification myNewIssuesNotificationMock1 = createMyNewIssuesNotificationMock();
374 MyNewIssuesNotification myNewIssuesNotificationMock2 = createMyNewIssuesNotificationMock();
375 doReturn(myNewIssuesNotificationMock1).doReturn(myNewIssuesNotificationMock2).when(notificationFactory).newMyNewIssuesNotification(any(assigneeCacheType));
377 TestComputationStepContext context = new TestComputationStepContext();
378 new SendIssueNotificationsStep(protoIssueCache, treeRootHolder, notificationService, analysisMetadataHolder, notificationFactory, db.getDbClient())
381 verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock1, myNewIssuesNotificationMock2));
382 // old API compatibility
383 verify(notificationService).deliver(myNewIssuesNotificationMock1);
384 verify(notificationService).deliver(myNewIssuesNotificationMock2);
386 verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
387 verify(notificationFactory, times(2)).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
388 verifyNoMoreInteractions(notificationFactory);
389 verifyAssigneeCache(assigneeCacheCaptor, perceval, arthur);
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);
396 ArgumentCaptor<UserDto> userCaptor2 = forClass(UserDto.class);
397 verify(myNewIssuesNotificationMock2).setAssignee(userCaptor2.capture());
398 myNewIssuesNotificationMocksByUsersName.put(userCaptor2.getValue().getLogin(), myNewIssuesNotificationMock2);
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);
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);
412 verifyStatistics(context, 1, 2, 0);
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))))
432 DiskCache.CacheAppender issueCache = this.protoIssueCache.newAppender();
433 issues.forEach(issueCache::append);
435 analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
436 when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
438 TestComputationStepContext context = new TestComputationStepContext();
439 underTest.execute(context);
441 verify(notificationService).deliver(newIssuesNotificationMock);
442 verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock));
443 // old API compatibility
444 verify(notificationService).deliver(myNewIssuesNotificationMock);
446 verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
447 verify(notificationFactory).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
448 verifyNoMoreInteractions(notificationFactory);
449 verifyAssigneeCache(assigneeCacheCaptor, user);
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);
462 verifyStatistics(context, 1, 1, 0);
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)
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);
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)))
484 when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
486 TestComputationStepContext context = new TestComputationStepContext();
487 underTest.execute(context);
489 verify(notificationService, never()).deliver(any(Notification.class));
490 verify(notificationService, never()).deliverEmails(anyCollection());
491 verifyStatistics(context, 0, 0, 0);
495 public void send_issues_change_notification() {
496 sendIssueChangeNotification(ANALYSE_DATE);
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);
509 TestComputationStepContext context = new TestComputationStepContext();
510 underTest.execute(context);
512 verify(notificationService, never()).deliver(any(Notification.class));
513 verify(notificationService, never()).deliverEmails(anyCollection());
514 verifyStatistics(context, 0, 0, 0);
518 public void send_issues_change_notification_even_if_issue_is_backdated() {
519 sendIssueChangeNotification(ANALYSE_DATE - FIVE_MINUTES_IN_MS);
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())
529 builder(Type.FILE, 11).setKey(file.getKey()).setName(file.longName()).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);
538 underTest.execute(new TestComputationStepContext());
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);
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);
560 public void send_issues_change_notification_on_branch() {
561 sendIssueChangeNotificationOnBranch(ANALYSE_DATE);
565 public void send_issues_change_notification_on_branch_even_if_issue_is_backdated() {
566 sendIssueChangeNotificationOnBranch(ANALYSE_DATE - FIVE_MINUTES_IN_MS);
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()
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));
589 underTest.execute(new TestComputationStepContext());
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);
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()))
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);
621 underTest.execute(new TestComputationStepContext());
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)
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));
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
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<>();
651 private NewIssuesFactoryCaptor(Supplier<IssuesChangesNotification> delegate) {
652 this.delegate = delegate;
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();
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);
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);
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);
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);
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()));
708 branch = newBranchComponent(project, newMainBranchDto(project, project.uuid()).setKey(BRANCH_NAME));
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());
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);
724 protected ComputationStep step() {