3 * Copyright (C) 2009-2020 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.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;
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;
109 public class SendIssueNotificationsStepTest extends BaseStepTest {
111 private static final String BRANCH_NAME = "feature";
112 private static final String PULL_REQUEST_ID = "pr-123";
114 private static final long ANALYSE_DATE = 123L;
115 private static final int FIVE_MINUTES_IN_MS = 1000 * 60 * 5;
117 private static final Duration ISSUE_DURATION = Duration.create(100L);
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();
125 public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule()
128 public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule()
129 .setBranch(new DefaultBranchImpl())
130 .setAnalysisDate(new Date(ANALYSE_DATE));
132 public TemporaryFolder temp = new TemporaryFolder();
134 public DbTester db = DbTester.create(System2.INSTANCE);
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();
153 private IssueCache issueCache;
154 private SendIssueNotificationsStep underTest;
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);
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);
170 TestComputationStepContext context = new TestComputationStepContext();
171 underTest.execute(context);
173 verify(notificationService, never()).deliver(any(Notification.class));
174 verify(notificationService, never()).deliverEmails(anyCollection());
175 verifyStatistics(context, 0, 0, 0);
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)))
185 when(notificationService.hasProjectSubscribersForTypes(eq(PROJECT.getUuid()), any())).thenReturn(true);
187 TestComputationStepContext context = new TestComputationStepContext();
188 underTest.execute(context);
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);
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))))
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);
217 TestComputationStepContext context = new TestComputationStepContext();
218 underTest.execute(context);
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);
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)))
240 when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
242 TestComputationStepContext context = new TestComputationStepContext();
243 underTest.execute(context);
245 verify(notificationService, never()).deliver(any(Notification.class));
246 verify(notificationService, never()).deliverEmails(anyCollection());
247 verifyStatistics(context, 0, 0, 0);
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));
260 TestComputationStepContext context = new TestComputationStepContext();
261 underTest.execute(context);
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);
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);
282 TestComputationStepContext context = new TestComputationStepContext();
283 underTest.execute(context);
285 verifyZeroInteractions(notificationService, newIssuesNotificationMock);
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));
298 TestComputationStepContext context = new TestComputationStepContext();
299 underTest.execute(context);
301 verify(notificationService, never()).deliver(any(Notification.class));
302 verify(notificationService, never()).deliverEmails(anyCollection());
303 verifyStatistics(context, 0, 0, 0);
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()));
311 issueCache.newAppender().append(
312 new DefaultIssue().setType(randomRuleType).setEffort(ISSUE_DURATION).setAssigneeUuid(user.getUuid())
313 .setCreationDate(new Date(ANALYSE_DATE)))
315 when(notificationService.hasProjectSubscribersForTypes(eq(PROJECT.getUuid()), any())).thenReturn(true);
317 TestComputationStepContext context = new TestComputationStepContext();
318 underTest.execute(context);
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);
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());
339 UserDto arthur = db.users().insertUser(u -> u.setLogin("arthur"));
340 Integer[] assignedToOther = IntStream.range(0, 3).mapToObj(i -> 10).toArray(Integer[]::new);
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))))
352 IssueCache issueCache = new IssueCache(temp.newFile(), System2.INSTANCE);
353 DiskCache<DefaultIssue>.DiskAppender newIssueCache = issueCache.newAppender();
354 issues.forEach(newIssueCache::append);
356 analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList()));
357 when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
359 NotificationFactory notificationFactory = mock(NotificationFactory.class);
360 NewIssuesNotification newIssuesNotificationMock = createNewIssuesNotificationMock();
361 when(notificationFactory.newNewIssuesNotification(assigneeCacheCaptor.capture()))
362 .thenReturn(newIssuesNotificationMock);
364 MyNewIssuesNotification myNewIssuesNotificationMock1 = createMyNewIssuesNotificationMock();
365 MyNewIssuesNotification myNewIssuesNotificationMock2 = createMyNewIssuesNotificationMock();
366 when(notificationFactory.newMyNewIssuesNotification(any(assigneeCacheType)))
367 .thenReturn(myNewIssuesNotificationMock1)
368 .thenReturn(myNewIssuesNotificationMock2);
370 TestComputationStepContext context = new TestComputationStepContext();
371 new SendIssueNotificationsStep(issueCache, treeRootHolder, notificationService, analysisMetadataHolder, notificationFactory, db.getDbClient())
374 verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock1, myNewIssuesNotificationMock2));
375 // old API compatibility
376 verify(notificationService).deliver(myNewIssuesNotificationMock1);
377 verify(notificationService).deliver(myNewIssuesNotificationMock2);
379 verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
380 verify(notificationFactory, times(2)).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
381 verifyNoMoreInteractions(notificationFactory);
382 verifyAssigneeCache(assigneeCacheCaptor, perceval, arthur);
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);
389 ArgumentCaptor<UserDto> userCaptor2 = forClass(UserDto.class);
390 verify(myNewIssuesNotificationMock2).setAssignee(userCaptor2.capture());
391 myNewIssuesNotificationMocksByUsersName.put(userCaptor2.getValue().getLogin(), myNewIssuesNotificationMock2);
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);
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);
405 verifyStatistics(context, 1, 2, 0);
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))))
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);
430 TestComputationStepContext context = new TestComputationStepContext();
431 underTest.execute(context);
433 verify(notificationService).deliver(newIssuesNotificationMock);
434 verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock));
435 // old API compatibility
436 verify(notificationService).deliver(myNewIssuesNotificationMock);
438 verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture());
439 verify(notificationFactory).newMyNewIssuesNotification(assigneeCacheCaptor.capture());
440 verifyNoMoreInteractions(notificationFactory);
441 verifyAssigneeCache(assigneeCacheCaptor, user);
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);
454 verifyStatistics(context, 1, 1, 0);
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)
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);
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)))
476 when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
478 TestComputationStepContext context = new TestComputationStepContext();
479 underTest.execute(context);
481 verify(notificationService, never()).deliver(any(Notification.class));
482 verify(notificationService, never()).deliverEmails(anyCollection());
483 verifyStatistics(context, 0, 0, 0);
487 public void send_issues_change_notification() {
488 sendIssueChangeNotification(ANALYSE_DATE);
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);
501 TestComputationStepContext context = new TestComputationStepContext();
502 underTest.execute(context);
504 verify(notificationService, never()).deliver(any(Notification.class));
505 verify(notificationService, never()).deliverEmails(anyCollection());
506 verifyStatistics(context, 0, 0, 0);
510 public void send_issues_change_notification_even_if_issue_is_backdated() {
511 sendIssueChangeNotification(ANALYSE_DATE - FIVE_MINUTES_IN_MS);
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())
521 builder(Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).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);
530 underTest.execute(new TestComputationStepContext());
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);
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);
552 public void send_issues_change_notification_on_branch() {
553 sendIssueChangeNotificationOnBranch(ANALYSE_DATE);
557 public void send_issues_change_notification_on_branch_even_if_issue_is_backdated() {
558 sendIssueChangeNotificationOnBranch(ANALYSE_DATE - FIVE_MINUTES_IN_MS);
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()
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));
581 underTest.execute(new TestComputationStepContext());
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);
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()))
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);
613 underTest.execute(new TestComputationStepContext());
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));
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
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<>();
642 private NewIssuesFactoryCaptor(Supplier<IssuesChangesNotification> delegate) {
643 this.delegate = delegate;
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();
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);
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);
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);
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);
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());
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);
710 protected ComputationStep step() {