@Override
public void execute(ComputationStep.Context context) {
Component project = treeRootHolder.getRoot();
+ NotificationStatistics notificationStatistics = new NotificationStatistics();
if (service.hasProjectSubscribersForTypes(project.getUuid(), NOTIF_TYPES)) {
- doExecute(project);
+ doExecute(notificationStatistics, project);
}
+ notificationStatistics.dumpTo(context);
}
- private void doExecute(Component project) {
+ private void doExecute(NotificationStatistics notificationStatistics, Component project) {
long analysisDate = analysisMetadataHolder.getAnalysisDate();
Predicate<DefaultIssue> isOnLeakPredicate = i -> i.isNew() && i.creationDate().getTime() >= truncateToSeconds(analysisDate);
NewIssuesStatistics newIssuesStats = new NewIssuesStatistics(isOnLeakPredicate);
List<String> assigneeUuids = stream(iterable.spliterator(), false).map(DefaultIssue::assignee).filter(Objects::nonNull).collect(toList());
usersDtoByUuids = dbClient.userDao().selectByUuids(dbSession, assigneeUuids).stream().collect(toMap(UserDto::getUuid, dto -> dto));
}
+
try (CloseableIterator<DefaultIssue> issues = issueCache.traverse()) {
- processIssues(newIssuesStats, issues, project, usersDtoByUuids);
+ processIssues(newIssuesStats, issues, project, usersDtoByUuids, notificationStatistics);
}
if (newIssuesStats.hasIssuesOnLeak()) {
- sendNewIssuesNotification(newIssuesStats, project, analysisDate);
- sendNewIssuesNotificationToAssignees(newIssuesStats, project, analysisDate);
+ sendNewIssuesNotification(newIssuesStats, project, analysisDate, notificationStatistics);
+ sendMyNewIssuesNotification(newIssuesStats, project, analysisDate, notificationStatistics);
}
}
return Date.from(instant).getTime();
}
- private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues, Component project, Map<String, UserDto> usersDtoByUuids) {
+ private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues, Component project, Map<String, UserDto> usersDtoByUuids,
+ NotificationStatistics notificationStatistics) {
while (issues.hasNext()) {
DefaultIssue issue = issues.next();
if (issue.type() != RuleType.SECURITY_HOTSPOT) {
if (issue.isNew() && issue.resolution() == null) {
newIssuesStats.add(issue);
} else if (issue.isChanged() && issue.mustSendNotifications()) {
- sendIssueChangeNotification(issue, project, usersDtoByUuids);
+ sendIssueChangeNotification(issue, project, usersDtoByUuids, notificationStatistics);
}
}
}
}
- private void sendIssueChangeNotification(DefaultIssue issue, Component project, Map<String, UserDto> usersDtoByUuids) {
+ private void sendIssueChangeNotification(DefaultIssue issue, Component project, Map<String, UserDto> usersDtoByUuids, NotificationStatistics notificationStatistics) {
IssueChangeNotification changeNotification = new IssueChangeNotification();
changeNotification.setRuleName(rules.getByKey(issue.ruleKey()).getName());
changeNotification.setIssue(issue);
changeNotification.setAssignee(usersDtoByUuids.get(issue.assignee()));
changeNotification.setProject(project.getPublicKey(), project.getName(), getBranchName(), getPullRequest());
getComponentKey(issue).ifPresent(c -> changeNotification.setComponent(c.getPublicKey(), c.getName()));
- service.deliver(changeNotification);
+ notificationStatistics.issueChangesDeliveries += service.deliver(changeNotification);
+ notificationStatistics.issueChanges++;
}
- private void sendNewIssuesNotification(NewIssuesStatistics statistics, Component project, long analysisDate) {
+ private void sendNewIssuesNotification(NewIssuesStatistics statistics, Component project, long analysisDate, NotificationStatistics notificationStatistics) {
NewIssuesStatistics.Stats globalStatistics = statistics.globalStatistics();
NewIssuesNotification notification = newIssuesNotificationFactory
.newNewIssuesNotification()
.setAnalysisDate(new Date(analysisDate))
.setStatistics(project.getName(), globalStatistics)
.setDebt(Duration.create(globalStatistics.effort().getOnLeak()));
- service.deliver(notification);
+ notificationStatistics.newIssuesDeliveries += service.deliver(notification);
+ notificationStatistics.newIssues++;
}
- private void sendNewIssuesNotificationToAssignees(NewIssuesStatistics statistics, Component project, long analysisDate) {
+ private void sendMyNewIssuesNotification(NewIssuesStatistics statistics, Component project, long analysisDate, NotificationStatistics notificationStatistics) {
Map<String, UserDto> userDtoByUuid = loadUserDtoByUuid(statistics);
statistics.getAssigneesStatistics().entrySet()
.stream()
.setStatistics(project.getName(), assigneeStatistics)
.setDebt(Duration.create(assigneeStatistics.effort().getOnLeak()));
- service.deliver(myNewIssuesNotification);
+ notificationStatistics.myNewIssuesDeliveries += service.deliver(myNewIssuesNotification);
+ notificationStatistics.myNewIssues++;
});
}
return branch.getType() == PULL_REQUEST ? analysisMetadataHolder.getPullRequestKey() : null;
}
+ private static class NotificationStatistics {
+ private int issueChanges = 0;
+ private int issueChangesDeliveries = 0;
+ private int newIssues = 0;
+ private int newIssuesDeliveries = 0;
+ private int myNewIssues = 0;
+ private int myNewIssuesDeliveries = 0;
+
+ private void dumpTo(ComputationStep.Context context) {
+ context.getStatistics()
+ .add("newIssuesNotifs", newIssues)
+ .add("newIssuesDeliveries", newIssuesDeliveries)
+ .add("myNewIssuesNotifs", myNewIssues)
+ .add("myNewIssuesDeliveries", myNewIssuesDeliveries)
+ .add("changesNotifs", issueChanges)
+ .add("changesDeliveries", issueChangesDeliveries);
+ }
+ }
}
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentCaptor;
-import org.sonar.api.notifications.Notification;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.Duration;
import org.sonar.api.utils.System2;
public void do_not_send_notifications_if_no_subscribers() {
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(false);
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
- verify(notificationService, never()).deliver(any(Notification.class));
+ verify(notificationService, never()).deliver(any());
+ verifyStatistics(context, 0, 0, 0);
}
@Test
.close();
when(notificationService.hasProjectSubscribersForTypes(eq(PROJECT.getUuid()), any())).thenReturn(true);
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
verify(notificationService).deliver(newIssuesNotificationMock);
verify(newIssuesNotificationMock).setProject(PROJECT.getPublicKey(), PROJECT.getName(), null, null);
verify(newIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
verify(newIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), any());
verify(newIssuesNotificationMock).setDebt(ISSUE_DURATION);
+ verifyStatistics(context, 1, 0, 0);
}
@Test
issues.forEach(issueCache::append);
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
verify(notificationService).deliver(newIssuesNotificationMock);
ArgumentCaptor<NewIssuesStatistics.Stats> statsCaptor = forClass(NewIssuesStatistics.Stats.class);
DistributedMetricStatsInt severity = stats.getDistributedMetricStats(NewIssuesStatistics.Metric.RULE_TYPE);
assertThat(severity.getOnLeak()).isEqualTo(efforts.length);
assertThat(severity.getTotal()).isEqualTo(backDatedEfforts.length + efforts.length);
+ verifyStatistics(context, 1, 0, 0);
}
@Test
.close();
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
- verify(notificationService, never()).deliver(any(Notification.class));
+ verify(notificationService, never()).deliver(any());
+ verifyStatistics(context, 0, 0, 0);
}
@Test
when(notificationService.hasProjectSubscribersForTypes(branch.uuid(), NOTIF_TYPES)).thenReturn(true);
analysisMetadataHolder.setBranch(newBranch());
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
verify(notificationService).deliver(newIssuesNotificationMock);
verify(newIssuesNotificationMock).setProject(branch.getKey(), branch.longName(), BRANCH_NAME, null);
verify(newIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
verify(newIssuesNotificationMock).setStatistics(eq(branch.longName()), any(NewIssuesStatistics.Stats.class));
verify(newIssuesNotificationMock).setDebt(ISSUE_DURATION);
+ verifyStatistics(context, 1, 0, 0);
}
@Test
analysisMetadataHolder.setBranch(newPullRequest());
analysisMetadataHolder.setPullRequestKey(PULL_REQUEST_ID);
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
verify(notificationService).deliver(newIssuesNotificationMock);
verify(newIssuesNotificationMock).setProject(branch.getKey(), branch.longName(), null, PULL_REQUEST_ID);
verify(newIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
verify(newIssuesNotificationMock).setStatistics(eq(branch.longName()), any(NewIssuesStatistics.Stats.class));
verify(newIssuesNotificationMock).setDebt(ISSUE_DURATION);
+ verifyStatistics(context, 1, 0, 0);
}
@Test
when(notificationService.hasProjectSubscribersForTypes(branch.uuid(), NOTIF_TYPES)).thenReturn(true);
analysisMetadataHolder.setBranch(newBranch());
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
- verify(notificationService, never()).deliver(any(Notification.class));
+ verify(notificationService, never()).deliver(any());
+ verifyStatistics(context, 0, 0, 0);
}
@Test
public void send_new_issues_notification_to_user() {
-
UserDto user = db.users().insertUser();
issueCache.newAppender().append(
.close();
when(notificationService.hasProjectSubscribersForTypes(eq(PROJECT.getUuid()), any())).thenReturn(true);
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
verify(notificationService).deliver(newIssuesNotificationMock);
verify(notificationService).deliver(myNewIssuesNotificationMock);
verify(myNewIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
verify(myNewIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), any(NewIssuesStatistics.Stats.class));
verify(myNewIssuesNotificationMock).setDebt(ISSUE_DURATION);
+ verifyStatistics(context, 1, 1, 0);
}
@Test
public void send_new_issues_notification_to_user_only_for_those_assigned_to_her() throws IOException {
-
UserDto perceval = db.users().insertUser(u -> u.setLogin("perceval"));
Integer[] assigned = IntStream.range(0, 5).mapToObj(i -> 10_000 * i).toArray(Integer[]::new);
Duration expectedEffort = Duration.create(stream(assigned).mapToInt(i -> i).sum());
MyNewIssuesNotification myNewIssuesNotificationMock2 = createMyNewIssuesNotificationMock();
when(newIssuesNotificationFactory.newMyNewIssuesNotification()).thenReturn(myNewIssuesNotificationMock1).thenReturn(myNewIssuesNotificationMock2);
+ TestComputationStepContext context = new TestComputationStepContext();
new SendIssueNotificationsStep(issueCache, ruleRepository, treeRootHolder, notificationService, analysisMetadataHolder, newIssuesNotificationFactory, db.getDbClient())
- .execute(new TestComputationStepContext());
+ .execute(context);
verify(notificationService).deliver(myNewIssuesNotificationMock1);
Map<String, MyNewIssuesNotification> myNewIssuesNotificationMocksByUsersName = new HashMap<>();
DistributedMetricStatsInt severity = stats.getDistributedMetricStats(NewIssuesStatistics.Metric.RULE_TYPE);
assertThat(severity.getOnLeak()).isEqualTo(assigned.length);
assertThat(severity.getTotal()).isEqualTo(assigned.length);
+
+ verifyStatistics(context, 1, 2, 0);
}
@Test
issues.forEach(issueCache::append);
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
verify(notificationService).deliver(newIssuesNotificationMock);
verify(notificationService).deliver(myNewIssuesNotificationMock);
DistributedMetricStatsInt severity = stats.getDistributedMetricStats(NewIssuesStatistics.Metric.RULE_TYPE);
assertThat(severity.getOnLeak()).isEqualTo(efforts.length);
assertThat(severity.getTotal()).isEqualTo(backDatedEfforts.length + efforts.length);
+
+ verifyStatistics(context, 1, 1, 0);
}
@Test
.close();
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true);
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
- verify(notificationService, never()).deliver(any(Notification.class));
+ verify(notificationService, never()).deliver(any());
+ verifyStatistics(context, 0, 0, 0);
}
@Test
RuleDefinitionDto ruleDefinitionDto = newRule();
DefaultIssue issue = prepareIssue(ANALYSE_DATE, user, project, file, ruleDefinitionDto, RuleType.SECURITY_HOTSPOT);
- underTest.execute(new TestComputationStepContext());
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
- verify(notificationService, never()).deliver(any(IssueChangeNotification.class));
+ verify(notificationService, never()).deliver(any());
+ verifyStatistics(context, 0, 0, 0);
}
@Test
return branch;
}
+ private static void verifyStatistics(TestComputationStepContext context, int expectedNewIssuesNotifications, int expectedMyNewIssuesNotifications, int expectedIssueChangesNotifications) {
+ context.getStatistics().assertValue("newIssuesNotifs", expectedNewIssuesNotifications);
+ context.getStatistics().assertValue("myNewIssuesNotifs", expectedMyNewIssuesNotifications);
+ context.getStatistics().assertValue("changesNotifs", expectedIssueChangesNotifications);
+ }
+
@Override
protected ComputationStep step() {
return underTest;
this(dbClient, new NotificationDispatcher[0]);
}
- public void deliver(Notification notification) {
+ public int deliver(Notification notification) {
SetMultimap<String, NotificationChannel> recipients = HashMultimap.create();
for (NotificationDispatcher dispatcher : dispatchers) {
NotificationDispatcher.Context context = new ContextImpl(recipients);
LOG.warn(String.format("Unable to dispatch notification %s using %s", notification, dispatcher), e);
}
}
- dispatch(notification, recipients);
+ return dispatch(notification, recipients);
}
- private static void dispatch(Notification notification, SetMultimap<String, NotificationChannel> recipients) {
+ private static int dispatch(Notification notification, SetMultimap<String, NotificationChannel> recipients) {
+ int count = 0;
for (Map.Entry<String, Collection<NotificationChannel>> entry : recipients.asMap().entrySet()) {
String username = entry.getKey();
Collection<NotificationChannel> userChannels = entry.getValue();
LOG.debug("For user {} via {}", username, userChannels);
for (NotificationChannel channel : userChannels) {
try {
- channel.deliver(notification, username);
+ if (channel.deliver(notification, username)) {
+ count++;
+ }
} catch (Exception e) {
// catch all exceptions in order to deliver via other channels
LOG.warn("Unable to deliver notification " + notification + " for user " + username + " via " + channel, e);
}
}
}
+ return count;
}
@VisibleForTesting
}
@Override
- public void deliver(Notification notification, String username) {
+ public boolean deliver(Notification notification, String username) {
User user = findByLogin(username);
if (user == null || StringUtils.isBlank(user.email())) {
LOG.debug("User does not exist or has no email: {}", username);
- return;
+ return false;
}
EmailMessage emailMessage = format(notification);
if (emailMessage != null) {
emailMessage.setTo(user.email());
- deliver(emailMessage);
+ return deliver(emailMessage);
}
+ return false;
}
@CheckForNull
return null;
}
- /**
- * Visibility has been relaxed for tests.
- */
- void deliver(EmailMessage emailMessage) {
+ boolean deliver(EmailMessage emailMessage) {
if (StringUtils.isBlank(configuration.getSmtpHost())) {
LOG.debug("SMTP host was not configured - email will not be sent");
- return;
+ return false;
}
try {
send(emailMessage);
+ return true;
} catch (EmailException e) {
LOG.error("Unable to send email", e);
+ return false;
}
}
private class FakeNotificationChannel extends NotificationChannel {
@Override
- public void deliver(Notification notification, String username) {
+ public boolean deliver(Notification notification, String username) {
+ return true;
}
}
.setTo("user@nowhere")
.setSubject("Foo")
.setMessage("Bar");
- underTest.deliver(emailMessage);
+ boolean delivered = underTest.deliver(emailMessage);
assertThat(smtpServer.getMessages()).isEmpty();
+ assertThat(delivered).isFalse();
}
@Test
.setTo("user@nowhere")
.setSubject("Review #3")
.setMessage("I'll take care of this violation.");
- underTest.deliver(emailMessage);
+ boolean delivered = underTest.deliver(emailMessage);
List<WiserMessage> messages = smtpServer.getMessages();
assertThat(messages).hasSize(1);
assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Review #3");
assertThat((String) email.getContent()).startsWith("I'll take care of this violation.");
+ assertThat(delivered).isTrue();
}
@Test
.setTo("user@nowhere")
.setSubject("Foo")
.setMessage("Bar");
- underTest.deliver(emailMessage);
+ boolean delivered = underTest.deliver(emailMessage);
List<WiserMessage> messages = smtpServer.getMessages();
assertThat(messages).hasSize(1);
assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Foo");
assertThat((String) email.getContent()).startsWith("Bar");
+ assertThat(delivered).isTrue();
}
@Test
.setTo("user@nowhere")
.setSubject("Foo")
.setMessage("Bar");
- underTest.deliver(emailMessage);
+ boolean delivered = underTest.deliver(emailMessage);
+
+ assertThat(delivered).isFalse();
}
@Test
}
@Override
- public void deliver(Notification notification, String username) {
+ public boolean deliver(Notification notification, String username) {
// do nothing
+ return true;
}
}
}
}
@Override
- public void deliver(org.sonar.api.notifications.Notification notification, String username) {
+ public boolean deliver(org.sonar.api.notifications.Notification notification, String username) {
// do nothing
+ return true;
}
}
}
}
@Override
- public void deliver(Notification notification, String username) {
+ public boolean deliver(Notification notification, String username) {
// do nothing
+ return true;
}
}
}
*
* @param notification the notification to deliver
* @param userlogin the login of the user who should receive the notification
+ * @return whether the notification was sent or not
*/
- public abstract void deliver(Notification notification, String userlogin);
+ public abstract boolean deliver(Notification notification, String userlogin);
@Override
public String toString() {