aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-server-common
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2019-04-16 15:19:27 +0200
committersonartech <sonartech@sonarsource.com>2019-04-23 10:37:57 +0200
commit58bb4b37da6e32a113870b0fc98d5494379641b6 (patch)
tree027172a0a7945ea6b2f3dae5d917e1fce1a66c81 /server/sonar-server-common
parentd9e7cb020409491b45199ab8762eb22746e3543d (diff)
downloadsonarqube-58bb4b37da6e32a113870b0fc98d5494379641b6.tar.gz
sonarqube-58bb4b37da6e32a113870b0fc98d5494379641b6.zip
SONAR-11757 single notification for FPs and changes on my issues
Diffstat (limited to 'server/sonar-server-common')
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java4
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java143
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplate.java146
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotification.java74
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/DoNotFixNotificationHandler.java104
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailMessage.java31
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailTemplate.java2
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrWontFixNotification.java96
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandler.java172
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplate.java101
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangeNotification.java142
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java276
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotification.java32
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java461
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationModule.java37
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java307
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NotificationWithProjectKeys.java47
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java110
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/notification/QGChangeEmailTemplate.java4
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandlerTest.java365
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplateTest.java721
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotificationTest.java73
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/DoNotFixNotificationHandlerTest.java293
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/EmailMessageTest.java49
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandlerTest.java498
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationTest.java92
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplateTest.java421
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssueChangeNotificationTest.java169
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest.java200
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilderTesting.java110
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationModuleTest.java37
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationTest.java35
32 files changed, 4125 insertions, 1227 deletions
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java
index e2c9f1be79c..3ab0fee2c86 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java
@@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
+import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.i18n.I18n;
@@ -71,6 +72,7 @@ public abstract class AbstractNewIssuesEmailTemplate implements EmailTemplate {
}
@Override
+ @CheckForNull
public EmailMessage format(Notification notification) {
if (shouldNotFormat(notification)) {
return null;
@@ -102,7 +104,7 @@ public abstract class AbstractNewIssuesEmailTemplate implements EmailTemplate {
return new EmailMessage()
.setMessageId(notification.getType() + "/" + notification.getFieldValue(FIELD_PROJECT_KEY))
.setSubject(subject(notification, computeFullProjectName(projectName, branchName)))
- .setMessage(message.toString());
+ .setPlainTextMessage(message.toString());
}
private static String computeFullProjectName(String projectName, @Nullable String branchName) {
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java
index 7d5e5243d74..23e7d6f9249 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandler.java
@@ -20,15 +20,17 @@
package org.sonar.server.issue.notification;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
import javax.annotation.CheckForNull;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
import org.sonar.server.notification.EmailNotificationHandler;
import org.sonar.server.notification.NotificationDispatcherMetadata;
import org.sonar.server.notification.NotificationManager;
@@ -36,12 +38,12 @@ import org.sonar.server.notification.NotificationManager.EmailRecipient;
import org.sonar.server.notification.email.EmailNotificationChannel;
import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
-import static org.sonar.core.util.stream.MoreCollectors.index;
import static org.sonar.core.util.stream.MoreCollectors.toSet;
-import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.core.util.stream.MoreCollectors.unorderedFlattenIndex;
+import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex;
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
-public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandler<IssueChangeNotification> {
+public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandler<IssuesChangesNotification> {
private static final String KEY = "ChangesOnMyIssue";
private static final NotificationDispatcherMetadata METADATA = NotificationDispatcherMetadata.create(KEY)
@@ -49,10 +51,13 @@ public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandle
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
private final NotificationManager notificationManager;
+ private final IssuesChangesNotificationSerializer serializer;
- public ChangesOnMyIssueNotificationHandler(NotificationManager notificationManager, EmailNotificationChannel emailNotificationChannel) {
+ public ChangesOnMyIssueNotificationHandler(NotificationManager notificationManager,
+ EmailNotificationChannel emailNotificationChannel, IssuesChangesNotificationSerializer serializer) {
super(emailNotificationChannel);
this.notificationManager = notificationManager;
+ this.serializer = serializer;
}
@Override
@@ -65,52 +70,110 @@ public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandle
}
@Override
- public Class<IssueChangeNotification> getNotificationClass() {
- return IssueChangeNotification.class;
+ public Class<IssuesChangesNotification> getNotificationClass() {
+ return IssuesChangesNotification.class;
}
@Override
- public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<IssueChangeNotification> notifications) {
- Multimap<String, IssueChangeNotification> notificationsByProjectKey = notifications.stream()
- // ignore inconsistent data
- .filter(t -> t.getProjectKey() != null)
- // ignore notification on which we can't identify who should be notified
- .filter(t -> t.getAssignee() != null)
- // do not notify users of the changes they made themselves (changeAuthor is null when change comes from an analysis)
- .filter(t -> !Objects.equals(t.getAssignee(), t.getChangeAuthor()))
- .collect(index(IssueChangeNotification::getProjectKey));
- if (notificationsByProjectKey.isEmpty()) {
+ public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<IssuesChangesNotification> notifications) {
+ Set<NotificationWithProjectKeys> notificationsWithPeerChangedIssues = notifications.stream()
+ .map(serializer::from)
+ // ignore notification of which the changeAuthor is the assignee of all changed issues
+ .filter(t -> t.getIssues().stream().anyMatch(issue -> issue.getAssignee().isPresent() && isPeerChanged(t.getChange(), issue)))
+ .map(NotificationWithProjectKeys::new)
+ .collect(Collectors.toSet());
+ if (notificationsWithPeerChangedIssues.isEmpty()) {
return ImmutableSet.of();
}
- return notificationsByProjectKey.asMap().entrySet()
+ Set<String> projectKeys = notificationsWithPeerChangedIssues.stream()
+ .flatMap(t -> t.getProjectKeys().stream())
+ .collect(Collectors.toSet());
+
+ // shortcut to save from building unnecessary data structures when all changed issues in notifications belong to
+ // the same project
+ if (projectKeys.size() == 1) {
+ Set<User> assigneesOfPeerChangedIssues = notificationsWithPeerChangedIssues.stream()
+ .flatMap(t -> t.getIssues().stream().filter(issue -> isPeerChanged(t.getChange(), issue)))
+ .map(ChangedIssue::getAssignee)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+ Set<EmailRecipient> subscribedAssignees = notificationManager.findSubscribedEmailRecipients(
+ KEY,
+ projectKeys.iterator().next(),
+ assigneesOfPeerChangedIssues.stream().map(User::getLogin).collect(Collectors.toSet()),
+ ALL_MUST_HAVE_ROLE_USER);
+
+ return subscribedAssignees.stream()
+ .flatMap(recipient -> notificationsWithPeerChangedIssues.stream()
+ // do not notify users of the changes they made themselves
+ .filter(notification -> !notification.getChange().isAuthorLogin(recipient.getLogin()))
+ .map(notification -> toEmailDeliveryRequest(notification, recipient, projectKeys)))
+ .filter(Objects::nonNull)
+ .collect(toSet(notificationsWithPeerChangedIssues.size()));
+ }
+
+ SetMultimap<String, String> assigneeLoginsOfPeerChangedIssuesByProjectKey = notificationsWithPeerChangedIssues.stream()
+ .flatMap(notification -> notification.getIssues().stream()
+ .filter(issue -> issue.getAssignee().isPresent())
+ .filter(issue -> isPeerChanged(notification.getChange(), issue)))
+ .collect(unorderedIndex(t -> t.getProject().getKey(), t -> t.getAssignee().get().getLogin()));
+
+ SetMultimap<String, EmailRecipient> authorizedAssigneeLoginsByProjectKey = assigneeLoginsOfPeerChangedIssuesByProjectKey.asMap().entrySet()
.stream()
- .flatMap(e -> toEmailDeliveryRequests(e.getKey(), e.getValue()))
- .collect(toSet(notifications.size()));
- }
+ .collect(unorderedFlattenIndex(
+ Map.Entry::getKey,
+ entry -> {
+ String projectKey = entry.getKey();
+ Set<String> assigneeLogins = (Set<String>) entry.getValue();
+ return notificationManager.findSubscribedEmailRecipients(KEY, projectKey, assigneeLogins, ALL_MUST_HAVE_ROLE_USER).stream();
+ }));
- private Stream<? extends EmailDeliveryRequest> toEmailDeliveryRequests(String projectKey, Collection<IssueChangeNotification> notifications) {
- Set<String> assignees = notifications.stream()
- .map(IssueChangeNotification::getAssignee)
- .collect(Collectors.toSet());
- Map<String, EmailRecipient> recipientsByLogin = notificationManager
- .findSubscribedEmailRecipients(KEY, projectKey, assignees, ALL_MUST_HAVE_ROLE_USER)
+ SetMultimap<EmailRecipient, String> projectKeyByRecipient = authorizedAssigneeLoginsByProjectKey.entries().stream()
+ .collect(unorderedIndex(Map.Entry::getValue, Map.Entry::getKey));
+
+ return projectKeyByRecipient.asMap().entrySet()
.stream()
- .collect(uniqueIndex(EmailRecipient::getLogin));
- return notifications.stream()
- .map(notification -> toEmailDeliveryRequest(recipientsByLogin, notification))
- .filter(Objects::nonNull);
+ .flatMap(entry -> {
+ EmailRecipient recipient = entry.getKey();
+ Set<String> subscribedProjectKeys = (Set<String>) entry.getValue();
+ return notificationsWithPeerChangedIssues.stream()
+ // do not notify users of the changes they made themselves
+ .filter(notification -> !notification.getChange().isAuthorLogin(recipient.getLogin()))
+ .map(notification -> toEmailDeliveryRequest(notification, recipient, subscribedProjectKeys))
+ .filter(Objects::nonNull);
+ })
+ .collect(toSet(notificationsWithPeerChangedIssues.size()));
}
+ /**
+ * Creates the {@link EmailDeliveryRequest} for the specified {@code recipient} with issues from the
+ * specified {@code notification} it is the assignee of.
+ *
+ * @return {@code null} when the recipient is the assignee of no issue in {@code notification}.
+ */
@CheckForNull
- private static EmailNotificationChannel.EmailDeliveryRequest toEmailDeliveryRequest(Map<String, EmailRecipient> recipientsByLogin,
- IssueChangeNotification notification) {
- String assignee = notification.getAssignee();
-
- EmailRecipient emailRecipient = recipientsByLogin.get(assignee);
- if (emailRecipient != null) {
- return new EmailNotificationChannel.EmailDeliveryRequest(emailRecipient.getEmail(), notification);
+ private static EmailDeliveryRequest toEmailDeliveryRequest(NotificationWithProjectKeys notification, EmailRecipient recipient, Set<String> subscribedProjectKeys) {
+ Set<ChangedIssue> recipientIssuesByProject = notification.getIssues().stream()
+ .filter(issue -> issue.getAssignee().filter(assignee -> recipient.getLogin().equals(assignee.getLogin())).isPresent())
+ .filter(issue -> subscribedProjectKeys.contains(issue.getProject().getKey()))
+ .collect(toSet(notification.getIssues().size()));
+ if (recipientIssuesByProject.isEmpty()) {
+ return null;
}
- return null;
+ return new EmailDeliveryRequest(
+ recipient.getEmail(),
+ new ChangesOnMyIssuesNotification(notification.getChange(), recipientIssuesByProject));
}
+
+ /**
+ * Is the author of the change the assignee of the specified issue?
+ * If not, it means the issue has been changed by a peer of the author of the change.
+ */
+ private static boolean isPeerChanged(Change change, ChangedIssue issue) {
+ Optional<User> assignee = issue.getAssignee();
+ return !assignee.isPresent() || !change.isAuthorLogin(assignee.get().getLogin());
+ }
+
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplate.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplate.java
new file mode 100644
index 00000000000..27346327def
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplate.java
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.SetMultimap;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.notifications.Notification;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.sonar.api.issue.Issue.STATUS_CLOSED;
+import static org.sonar.api.issue.Issue.STATUS_OPEN;
+import static org.sonar.core.util.stream.MoreCollectors.index;
+
+/**
+ * Creates email message for notification "Changes on my issues".
+ */
+public class ChangesOnMyIssuesEmailTemplate extends IssueChangesEmailTemplate {
+ private static final String NOTIFICATION_NAME_I18N_KEY = "notification.dispatcher.ChangesOnMyIssue";
+
+ public ChangesOnMyIssuesEmailTemplate(I18n i18n, EmailSettings settings) {
+ super(i18n, settings);
+ }
+
+ @Override
+ @CheckForNull
+ public EmailMessage format(Notification notif) {
+ if (!(notif instanceof ChangesOnMyIssuesNotification)) {
+ return null;
+ }
+
+ ChangesOnMyIssuesNotification notification = (ChangesOnMyIssuesNotification) notif;
+
+ if (notification.getChange() instanceof AnalysisChange) {
+ checkState(!notification.getChangedIssues().isEmpty(), "changedIssues can't be empty");
+ return formatAnalysisNotification(notification.getChangedIssues().keySet().iterator().next(), notification);
+ }
+ return formatMultiProject(notification);
+ }
+
+ private EmailMessage formatAnalysisNotification(Project project, ChangesOnMyIssuesNotification notification) {
+ return new EmailMessage()
+ .setMessageId("changes-on-my-issues/" + project.getKey())
+ .setSubject(buildAnalysisSubject(project))
+ .setHtmlMessage(buildAnalysisMessage(project, notification));
+ }
+
+ private static String buildAnalysisSubject(Project project) {
+ StringBuilder res = new StringBuilder("Analysis has changed some of your issues in ");
+ toString(res, project);
+ return res.toString();
+ }
+
+ private String buildAnalysisMessage(Project project, ChangesOnMyIssuesNotification notification) {
+ String projectParams = toUrlParams(project);
+
+ StringBuilder sb = new StringBuilder();
+ paragraph(sb, s -> s.append("Hi,"));
+ paragraph(sb, s -> s.append("An analysis has updated ").append(issuesOrAnIssue(notification.getChangedIssues()))
+ .append(" assigned to you:"));
+
+ ListMultimap<String, ChangedIssue> issuesByNewStatus = notification.getChangedIssues().values().stream()
+ .collect(index(changedIssue -> STATUS_CLOSED.equals(changedIssue.getNewStatus()) ? STATUS_CLOSED : STATUS_OPEN, t -> t));
+
+ List<ChangedIssue> closedIssues = issuesByNewStatus.get(STATUS_CLOSED);
+ if (!closedIssues.isEmpty()) {
+ paragraph(sb, s -> s.append("Closed ").append(issueOrIssues(closedIssues)).append(":"));
+ addIssuesByRule(sb, closedIssues, projectIssuePageHref(projectParams));
+ }
+ List<ChangedIssue> openIssues = issuesByNewStatus.get(STATUS_OPEN);
+ if (!openIssues.isEmpty()) {
+ paragraph(sb, s -> s.append("Open ").append(issueOrIssues(openIssues)).append(":"));
+ addIssuesByRule(sb, openIssues, projectIssuePageHref(projectParams));
+ }
+
+ addFooter(sb, NOTIFICATION_NAME_I18N_KEY);
+
+ return sb.toString();
+ }
+
+ private EmailMessage formatMultiProject(ChangesOnMyIssuesNotification notification) {
+ User user = ((UserChange) notification.getChange()).getUser();
+ return new EmailMessage()
+ .setFrom(user.getName().orElse(user.getLogin()))
+ .setMessageId("changes-on-my-issues")
+ .setSubject("A manual update has changed some of your issues")
+ .setHtmlMessage(buildMultiProjectMessage(notification));
+ }
+
+ private String buildMultiProjectMessage(ChangesOnMyIssuesNotification notification) {
+ StringBuilder sb = new StringBuilder();
+ paragraph(sb, s -> s.append("Hi,"));
+ paragraph(sb, s -> {
+ SetMultimap<Project, ChangedIssue> changedIssues = notification.getChangedIssues();
+ s.append("A manual change has updated ").append(issuesOrAnIssue(changedIssues))
+ .append(" assigned to you:");
+ });
+
+ addIssuesByProjectThenRule(sb, notification.getChangedIssues());
+
+ addFooter(sb, NOTIFICATION_NAME_I18N_KEY);
+
+ return sb.toString();
+ }
+
+ private static String issueOrIssues(Collection<?> collection) {
+ if (collection.size() > 1) {
+ return "issues";
+ }
+ return "issue";
+ }
+
+ private static String issuesOrAnIssue(SetMultimap<Project, ChangedIssue> changedIssues) {
+ if (changedIssues.size() > 1) {
+ return "issues";
+ }
+ return "an issue";
+ }
+
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotification.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotification.java
new file mode 100644
index 00000000000..aba506cbce9
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotification.java
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.SetMultimap;
+import java.util.Collection;
+import java.util.Objects;
+import org.sonar.api.notifications.Notification;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+
+import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex;
+
+/**
+ * This notification is never serialized to DB.
+ * <p>
+ * It is derived from {@link IssuesChangesNotification} by
+ * {@link FPOrWontFixNotificationHandler} and extends {@link Notification} only to comply with
+ * {@link org.sonar.server.issue.notification.EmailTemplate#format(Notification)} API.
+ */
+class ChangesOnMyIssuesNotification extends Notification {
+ private final Change change;
+ private final SetMultimap<Project, ChangedIssue> changedIssues;
+
+ public ChangesOnMyIssuesNotification(Change change, Collection<ChangedIssue> changedIssues) {
+ super("ChangesOnMyIssues");
+ this.change = change;
+ this.changedIssues = changedIssues.stream().collect(unorderedIndex(ChangedIssue::getProject, t -> t));
+ }
+
+ public Change getChange() {
+ return change;
+ }
+
+ public SetMultimap<Project, ChangedIssue> getChangedIssues() {
+ return changedIssues;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ChangesOnMyIssuesNotification that = (ChangesOnMyIssuesNotification) o;
+ return Objects.equals(change, that.change) &&
+ Objects.equals(changedIssues, that.changedIssues);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(change, changedIssues);
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/DoNotFixNotificationHandler.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/DoNotFixNotificationHandler.java
deleted file mode 100644
index fc86ef73da2..00000000000
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/DoNotFixNotificationHandler.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.server.issue.notification;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimap;
-import java.util.Collection;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Stream;
-import org.sonar.api.issue.Issue;
-import org.sonar.server.notification.EmailNotificationHandler;
-import org.sonar.server.notification.NotificationDispatcherMetadata;
-import org.sonar.server.notification.NotificationManager;
-import org.sonar.server.notification.NotificationManager.EmailRecipient;
-import org.sonar.server.notification.email.EmailNotificationChannel;
-import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
-
-import static java.util.Collections.emptySet;
-import static java.util.Optional.of;
-import static org.sonar.core.util.stream.MoreCollectors.index;
-import static org.sonar.core.util.stream.MoreCollectors.toSet;
-import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
-
-public class DoNotFixNotificationHandler extends EmailNotificationHandler<IssueChangeNotification> {
-
- public static final String KEY = "NewFalsePositiveIssue";
- private static final NotificationDispatcherMetadata METADATA = NotificationDispatcherMetadata.create(KEY)
- .setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(false))
- .setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
-
- private static final Set<String> SUPPORTED_NEW_RESOLUTIONS = ImmutableSet.of(Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_WONT_FIX);
-
- private final NotificationManager notificationManager;
-
- public DoNotFixNotificationHandler(NotificationManager notificationManager, EmailNotificationChannel emailNotificationChannel) {
- super(emailNotificationChannel);
- this.notificationManager = notificationManager;
- }
-
- @Override
- public Optional<NotificationDispatcherMetadata> getMetadata() {
- return of(METADATA);
- }
-
- public static NotificationDispatcherMetadata newMetadata() {
- return METADATA;
- }
-
- @Override
- public Class<IssueChangeNotification> getNotificationClass() {
- return IssueChangeNotification.class;
- }
-
- @Override
- public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<IssueChangeNotification> notifications) {
- Multimap<String, IssueChangeNotification> notificationsByProjectKey = notifications.stream()
- // ignore inconsistent data
- .filter(t -> t.getProjectKey() != null)
- // ignore notification on which we can't identify who should not be notified
- // (and anyway, it should not be null as an analysis can not resolve an issue as FP or Won't fix)
- .filter(t -> t.getChangeAuthor() != null)
- // ignore changes which did not lead to a FP or Won't Fix resolution
- .filter(t -> SUPPORTED_NEW_RESOLUTIONS.contains(t.getNewResolution()))
- .collect(index(IssueChangeNotification::getProjectKey));
- if (notificationsByProjectKey.isEmpty()) {
- return emptySet();
- }
-
- return notificationsByProjectKey.asMap().entrySet()
- .stream()
- .flatMap(e -> toEmailDeliveryRequests(e.getKey(), e.getValue()))
- .collect(toSet(notifications.size()));
- }
-
- private Stream<? extends EmailDeliveryRequest> toEmailDeliveryRequests(String projectKey, Collection<IssueChangeNotification> notifications) {
- Set<EmailRecipient> recipients = notificationManager
- .findSubscribedEmailRecipients(KEY, projectKey, ALL_MUST_HAVE_ROLE_USER);
- return notifications.stream()
- .flatMap(notification -> recipients.stream()
- // do not notify author of the change
- .filter(t -> !Objects.equals(t.getLogin(), notification.getChangeAuthor()))
- .map(t -> new EmailDeliveryRequest(t.getEmail(), notification)));
- }
-
-}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailMessage.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailMessage.java
index 2bbc4f5b3b7..e2cc3304060 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailMessage.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailMessage.java
@@ -23,11 +23,12 @@ import org.apache.commons.lang.builder.ToStringBuilder;
public class EmailMessage {
- private String from;
- private String to;
- private String subject;
- private String message;
- private String messageId;
+ private String from = null;
+ private String to = null;
+ private String subject = null;
+ private String message = null;
+ private boolean html = false;
+ private String messageId = null;
/**
* @param from full name of user, who initiated this message or null, if message was initiated by Sonar
@@ -77,13 +78,25 @@ public class EmailMessage {
/**
* @param message message body
*/
- public EmailMessage setMessage(String message) {
+ public EmailMessage setPlainTextMessage(String message) {
this.message = message;
+ this.html = false;
return this;
}
/**
- * @see #setMessage(String)
+ * @param message HTML message body
+ */
+ public EmailMessage setHtmlMessage(String message) {
+ this.message = message;
+ this.html = true;
+ return this;
+ }
+
+ /**
+ * Either plain text or HTML.
+ * @see #setPlainTextMessage(String) (String)
+ * @see #setHtmlMessage(String) (String) (String)
*/
public String getMessage() {
return message;
@@ -104,6 +117,10 @@ public class EmailMessage {
return messageId;
}
+ public boolean isHtml() {
+ return html;
+ }
+
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailTemplate.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailTemplate.java
index eaa7af95bdb..79dd3036d4b 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailTemplate.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/EmailTemplate.java
@@ -19,6 +19,7 @@
*/
package org.sonar.server.issue.notification;
+import javax.annotation.CheckForNull;
import org.sonar.api.ExtensionPoint;
import org.sonar.api.server.ServerSide;
import org.sonar.api.notifications.Notification;
@@ -27,6 +28,7 @@ import org.sonar.api.notifications.Notification;
@ExtensionPoint
public interface EmailTemplate {
+ @CheckForNull
EmailMessage format(Notification notification);
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrWontFixNotification.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrWontFixNotification.java
new file mode 100644
index 00000000000..d15b18ba8d6
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrWontFixNotification.java
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.SetMultimap;
+import java.util.Collection;
+import java.util.Objects;
+import org.sonar.api.notifications.Notification;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+
+import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex;
+
+/**
+ * This notification is never serialized to DB.
+ * <p>
+ * It is derived from {@link IssuesChangesNotification} by
+ * {@link FPOrWontFixNotificationHandler} and extends {@link Notification} only to comply with
+ * {@link org.sonar.server.issue.notification.EmailTemplate#format(Notification)} API.
+ */
+class FPOrWontFixNotification extends Notification {
+ private static final String KEY = "FPorWontFix";
+
+ public enum FpOrWontFix {
+ FP, WONT_FIX
+ }
+
+ private final Change change;
+ private final SetMultimap<Project, ChangedIssue> changedIssues;
+ private final FpOrWontFix resolution;
+
+ public FPOrWontFixNotification(Change change, Collection<ChangedIssue> changedIssues, FpOrWontFix resolution) {
+ super(KEY);
+ this.changedIssues = changedIssues.stream().collect(unorderedIndex(ChangedIssue::getProject, t -> t));
+ this.change = change;
+ this.resolution = resolution;
+ }
+
+ public Change getChange() {
+ return change;
+ }
+
+ public SetMultimap<Project, ChangedIssue> getChangedIssues() {
+ return changedIssues;
+ }
+
+ public FpOrWontFix getResolution() {
+ return resolution;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ FPOrWontFixNotification that = (FPOrWontFixNotification) o;
+ return Objects.equals(changedIssues, that.changedIssues) &&
+ Objects.equals(change, that.change) &&
+ resolution == that.resolution;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(changedIssues, change, resolution);
+ }
+
+ @Override
+ public String toString() {
+ return "FPOrWontFixNotification{" +
+ "changedIssues=" + changedIssues +
+ ", change=" + change +
+ ", resolution=" + resolution +
+ '}';
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandler.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandler.java
new file mode 100644
index 00000000000..7b56c140d58
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandler.java
@@ -0,0 +1,172 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.SetMultimap;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.issue.Issue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.notification.EmailNotificationHandler;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.notification.NotificationManager.EmailRecipient;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
+
+import static com.google.common.collect.Sets.intersection;
+import static java.util.Collections.emptySet;
+import static java.util.Optional.of;
+import static java.util.Optional.ofNullable;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+import static org.sonar.core.util.stream.MoreCollectors.unorderedFlattenIndex;
+import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex;
+import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP;
+import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX;
+import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
+
+public class FPOrWontFixNotificationHandler extends EmailNotificationHandler<IssuesChangesNotification> {
+
+ public static final String KEY = "NewFalsePositiveIssue";
+ private static final NotificationDispatcherMetadata METADATA = NotificationDispatcherMetadata.create(KEY)
+ .setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(false))
+ .setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
+
+ private static final Set<String> FP_OR_WONTFIX_RESOLUTIONS = ImmutableSet.of(Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_WONT_FIX);
+
+ private final NotificationManager notificationManager;
+ private final IssuesChangesNotificationSerializer serializer;
+
+ public FPOrWontFixNotificationHandler(NotificationManager notificationManager,
+ EmailNotificationChannel emailNotificationChannel, IssuesChangesNotificationSerializer serializer) {
+ super(emailNotificationChannel);
+ this.notificationManager = notificationManager;
+ this.serializer = serializer;
+ }
+
+ @Override
+ public Optional<NotificationDispatcherMetadata> getMetadata() {
+ return of(METADATA);
+ }
+
+ public static NotificationDispatcherMetadata newMetadata() {
+ return METADATA;
+ }
+
+ @Override
+ public Class<IssuesChangesNotification> getNotificationClass() {
+ return IssuesChangesNotification.class;
+ }
+
+ @Override
+ public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<IssuesChangesNotification> notifications) {
+ Set<NotificationWithProjectKeys> changeNotificationsWithFpOrWontFix = notifications.stream()
+ .map(serializer::from)
+ // ignore notifications which contain no issue changed to a FP or Won't Fix resolution
+ .filter(t -> t.getIssues().stream()
+ .filter(issue -> issue.getNewResolution().isPresent())
+ .anyMatch(issue -> FP_OR_WONTFIX_RESOLUTIONS.contains(issue.getNewResolution().get())))
+ .map(NotificationWithProjectKeys::new)
+ .collect(Collectors.toSet());
+ if (changeNotificationsWithFpOrWontFix.isEmpty()) {
+ return emptySet();
+ }
+ Set<String> projectKeys = changeNotificationsWithFpOrWontFix.stream()
+ .flatMap(t -> t.getProjectKeys().stream())
+ .collect(Collectors.toSet());
+
+ // shortcut to save from building unnecessary data structures when all changed issues in notifications belong to
+ // the same project
+ if (projectKeys.size() == 1) {
+ Set<EmailRecipient> recipients = notificationManager.findSubscribedEmailRecipients(KEY, projectKeys.iterator().next(), ALL_MUST_HAVE_ROLE_USER);
+ return changeNotificationsWithFpOrWontFix.stream()
+ .flatMap(notification -> toRequests(notification, projectKeys, recipients))
+ .collect(toSet(changeNotificationsWithFpOrWontFix.size()));
+ }
+
+ Set<EmailRecipientAndProject> recipientsByProjectKey = projectKeys.stream()
+ .flatMap(projectKey -> notificationManager.findSubscribedEmailRecipients(KEY, projectKey, ALL_MUST_HAVE_ROLE_USER).stream()
+ .map(emailRecipient -> new EmailRecipientAndProject(emailRecipient, projectKey)))
+ .collect(Collectors.toSet());
+
+ // builds sets of projectKeys for which a given recipient has subscribed to
+ SetMultimap<EmailRecipient, String> projectKeysByRecipient = recipientsByProjectKey.stream()
+ .collect(unorderedIndex(t -> t.recipient, t -> t.projectKey));
+ // builds sets of recipients who subscribed to the same subset of projects
+ Multimap<Set<String>, EmailRecipient> recipientsBySubscribedProjects = projectKeysByRecipient.asMap()
+ .entrySet().stream()
+ .collect(unorderedIndex(t -> (Set<String>) t.getValue(), Map.Entry::getKey));
+
+ return changeNotificationsWithFpOrWontFix.stream()
+ .flatMap(notification -> {
+ // builds sets of recipients for each sub group of the notification's projectKeys necessary
+ SetMultimap<Set<String>, EmailRecipient> recipientsByProjectKeys = recipientsBySubscribedProjects.asMap().entrySet()
+ .stream()
+ .collect(unorderedFlattenIndex(t -> intersection(t.getKey(), notification.getProjectKeys()).immutableCopy(), t -> t.getValue().stream()));
+ return recipientsByProjectKeys.asMap().entrySet().stream()
+ .flatMap(entry -> toRequests(notification, entry.getKey(), entry.getValue()));
+ })
+ .collect(toSet(changeNotificationsWithFpOrWontFix.size()));
+ }
+
+ private static Stream<EmailDeliveryRequest> toRequests(NotificationWithProjectKeys notification, Set<String> projectKeys, Collection<EmailRecipient> recipients) {
+ return recipients.stream()
+ // do not notify author of the change
+ .filter(recipient -> !notification.getChange().isAuthorLogin(recipient.getLogin()))
+ .flatMap(recipient -> {
+ SetMultimap<String, ChangedIssue> issuesByNewResolution = notification.getIssues().stream()
+ // ignore issues not changed to a FP or Won't Fix resolution
+ .filter(issue -> issue.getNewResolution().filter(FP_OR_WONTFIX_RESOLUTIONS::contains).isPresent())
+ // ignore issues belonging to projects the recipients have not subscribed to
+ .filter(issue -> projectKeys.contains(issue.getProject().getKey()))
+ .collect(unorderedIndex(t -> t.getNewResolution().get(), issue -> issue));
+
+ return Stream.of(
+ ofNullable(issuesByNewResolution.get(Issue.RESOLUTION_FALSE_POSITIVE))
+ .filter(t -> !t.isEmpty())
+ .map(fpIssues -> new FPOrWontFixNotification(notification.getChange(), fpIssues, FP))
+ .orElse(null),
+ ofNullable(issuesByNewResolution.get(Issue.RESOLUTION_WONT_FIX))
+ .filter(t -> !t.isEmpty())
+ .map(wontFixIssues -> new FPOrWontFixNotification(notification.getChange(), wontFixIssues, WONT_FIX))
+ .orElse(null))
+ .filter(Objects::nonNull)
+ .map(fpOrWontFixNotification -> new EmailDeliveryRequest(recipient.getEmail(), fpOrWontFixNotification));
+ });
+ }
+
+ private static final class EmailRecipientAndProject {
+ private final EmailRecipient recipient;
+ private final String projectKey;
+
+ private EmailRecipientAndProject(EmailRecipient recipient, String projectKey) {
+ this.recipient = recipient;
+ this.projectKey = projectKey;
+ }
+ }
+
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplate.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplate.java
new file mode 100644
index 00000000000..1c368981df9
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplate.java
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import javax.annotation.CheckForNull;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.notifications.Notification;
+import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
+
+import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP;
+import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX;
+
+/**
+ * Creates email message for notification "issue-changes".
+ */
+public class FpOrWontFixEmailTemplate extends IssueChangesEmailTemplate {
+
+ private static final String NOTIFICATION_NAME_I18N_KEY = "notification.dispatcher.NewFalsePositiveIssue";
+
+ public FpOrWontFixEmailTemplate(I18n i18n, EmailSettings settings) {
+ super(i18n, settings);
+ }
+
+ @Override
+ @CheckForNull
+ public EmailMessage format(Notification notif) {
+ if (!(notif instanceof FPOrWontFixNotification)) {
+ return null;
+ }
+
+ FPOrWontFixNotification notification = (FPOrWontFixNotification) notif;
+
+ EmailMessage emailMessage = new EmailMessage()
+ .setMessageId(getMessageId(notification.getResolution()))
+ .setSubject(buildSubject(notification))
+ .setHtmlMessage(buildMessage(notification));
+ if (notification.getChange() instanceof UserChange) {
+ User user = ((UserChange) notification.getChange()).getUser();
+ emailMessage.setFrom(user.getName().orElse(user.getLogin()));
+ }
+ return emailMessage;
+ }
+
+ private static String getMessageId(FpOrWontFix resolution) {
+ if (resolution == WONT_FIX) {
+ return "wontfix-issue-changes";
+ }
+ if (resolution == FP) {
+ return "fp-issue-changes";
+ }
+ throw new IllegalArgumentException("Unsupported resolution " + resolution);
+ }
+
+ private static String buildSubject(FPOrWontFixNotification notification) {
+ return "Issues marked as " + resolutionLabel(notification.getResolution());
+ }
+
+ private String buildMessage(FPOrWontFixNotification notification) {
+ StringBuilder sb = new StringBuilder();
+ paragraph(sb, s -> s.append("Hi,"));
+ paragraph(sb, s -> s.append("A manual change has resolved ").append(notification.getChangedIssues().size() > 1 ? "issues" : "an issue")
+ .append(" as ").append(resolutionLabel(notification.getResolution())).append(":"));
+
+ addIssuesByProjectThenRule(sb, notification.getChangedIssues());
+
+ addFooter(sb, NOTIFICATION_NAME_I18N_KEY);
+
+ return sb.toString();
+ }
+
+ private static String resolutionLabel(FpOrWontFix resolution) {
+ if (resolution == WONT_FIX) {
+ return "Won't Fix";
+ }
+ if (resolution == FP) {
+ return "False Positive";
+ }
+ throw new IllegalArgumentException("Unsupported resolution " + resolution);
+ }
+
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangeNotification.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangeNotification.java
deleted file mode 100644
index e7c1163b3ac..00000000000
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangeNotification.java
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.server.issue.notification;
-
-import com.google.common.base.Strings;
-import java.io.Serializable;
-import java.util.Map;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.sonar.api.notifications.Notification;
-import org.sonar.core.issue.DefaultIssue;
-import org.sonar.core.issue.FieldDiffs;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.db.user.UserDto;
-
-import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_BRANCH;
-import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PROJECT_KEY;
-import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PROJECT_NAME;
-import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PULL_REQUEST;
-
-public class IssueChangeNotification extends Notification {
-
- public static final String TYPE = "issue-changes";
- private static final String FIELD_CHANGE_AUTHOR = "changeAuthor";
- private static final String FIELD_ASSIGNEE = "assignee";
-
- public IssueChangeNotification() {
- super(TYPE);
- }
-
- public IssueChangeNotification setIssue(DefaultIssue issue) {
- setFieldValue("key", issue.key());
- setFieldValue("message", issue.message());
- FieldDiffs currentChange = issue.currentChange();
- if (currentChange != null) {
- for (Map.Entry<String, FieldDiffs.Diff> entry : currentChange.diffs().entrySet()) {
- String type = entry.getKey();
- FieldDiffs.Diff diff = entry.getValue();
- setFieldValue("old." + type, neverEmptySerializableToString(diff.oldValue()));
- setFieldValue("new." + type, neverEmptySerializableToString(diff.newValue()));
- }
- }
- return this;
- }
-
- @CheckForNull
- public String getNewResolution() {
- return getFieldValue("new.resolution");
- }
-
- public IssueChangeNotification setProject(ComponentDto project) {
- return setProject(project.getKey(), project.name(), project.getBranch(), project.getPullRequest());
- }
-
- public IssueChangeNotification setProject(String projectKey, String projectName, @Nullable String branch, @Nullable String pullRequest) {
- setFieldValue(FIELD_PROJECT_NAME, projectName);
- setFieldValue(FIELD_PROJECT_KEY, projectKey);
- if (branch != null) {
- setFieldValue(FIELD_BRANCH, branch);
- }
- if (pullRequest != null) {
- setFieldValue(FIELD_PULL_REQUEST, pullRequest);
- }
- return this;
- }
-
- @CheckForNull
- public String getProjectKey() {
- return getFieldValue(FIELD_PROJECT_KEY);
- }
-
- public IssueChangeNotification setComponent(ComponentDto component) {
- return setComponent(component.getKey(), component.longName());
- }
-
- public IssueChangeNotification setComponent(String componentKey, String componentName) {
- setFieldValue("componentName", componentName);
- setFieldValue("componentKey", componentKey);
- return this;
- }
-
- public IssueChangeNotification setChangeAuthor(@Nullable UserDto author) {
- if (author == null) {
- return this;
- }
- setFieldValue(FIELD_CHANGE_AUTHOR, author.getLogin());
- return this;
- }
-
- @CheckForNull
- public String getChangeAuthor() {
- return getFieldValue(FIELD_CHANGE_AUTHOR);
- }
-
- public IssueChangeNotification setRuleName(@Nullable String s) {
- if (s != null) {
- setFieldValue("ruleName", s);
- }
- return this;
- }
-
- public IssueChangeNotification setComment(@Nullable String s) {
- if (s != null) {
- setFieldValue("comment", s);
- }
- return this;
- }
-
- @CheckForNull
- private static String neverEmptySerializableToString(@Nullable Serializable s) {
- return s != null ? Strings.emptyToNull(s.toString()) : null;
- }
-
- public IssueChangeNotification setAssignee(@Nullable UserDto assignee) {
- if (assignee != null) {
- setFieldValue(FIELD_ASSIGNEE, assignee.getLogin());
- }
- return this;
- }
-
- @CheckForNull
- public String getAssignee() {
- return getFieldValue(FIELD_ASSIGNEE);
- }
-}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java
index 9e8a7f6a06c..3266d2cf051 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java
@@ -19,162 +19,190 @@
*/
package org.sonar.server.issue.notification;
-import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.SetMultimap;
import java.io.UnsupportedEncodingException;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.SortedSet;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
import org.sonar.api.config.EmailSettings;
-import org.sonar.api.notifications.Notification;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.user.UserDto;
+import org.sonar.api.i18n.I18n;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
import static java.net.URLEncoder.encode;
-import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_BRANCH;
-import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PULL_REQUEST;
-
-/**
- * Creates email message for notification "issue-changes".
- */
-public class IssueChangesEmailTemplate implements EmailTemplate {
-
- private static final char NEW_LINE = '\n';
- private final DbClient dbClient;
+import static org.sonar.core.util.stream.MoreCollectors.index;
+
+public abstract class IssueChangesEmailTemplate implements EmailTemplate {
+
+ private static final Comparator<Rule> RULE_COMPARATOR = Comparator.comparing(r -> r.getKey().toString());
+ private static final Comparator<Project> PROJECT_COMPARATOR = Comparator.comparing(Project::getProjectName)
+ .thenComparing(t -> t.getBranchName().orElse(""));
+ private static final Comparator<ChangedIssue> CHANGED_ISSUE_KEY_COMPARATOR = Comparator.comparing(ChangedIssue::getKey, Comparator.naturalOrder());
+ /**
+ * Assuming:
+ * <ul>
+ * <li>UUID length of 40 chars</li>
+ * <li>a max URL length of 2083 chars</li>
+ * </ul>
+ * This leaves ~850 chars for the rest of the URL (including other parameters such as the project key and the branch),
+ * which is reasonable to stay safe from the max URL length supported by some browsers and network devices.
+ */
+ private static final int MAX_ISSUES_BY_LINK = 40;
+ private static final String URL_ENCODED_COMMA = urlEncode(",");
+
+ private final I18n i18n;
private final EmailSettings settings;
- public IssueChangesEmailTemplate(DbClient dbClient, EmailSettings settings) {
- this.dbClient = dbClient;
+ protected IssueChangesEmailTemplate(I18n i18n, EmailSettings settings) {
+ this.i18n = i18n;
this.settings = settings;
}
- @Override
- public EmailMessage format(Notification notif) {
- if (!IssueChangeNotification.TYPE.equals(notif.getType())) {
- return null;
- }
-
- StringBuilder sb = new StringBuilder();
- appendHeader(notif, sb);
- sb.append(NEW_LINE);
- appendChanges(notif, sb);
- sb.append(NEW_LINE);
- appendFooter(sb, notif);
-
- String projectName = notif.getFieldValue("projectName");
- String issueKey = notif.getFieldValue("key");
- String author = notif.getFieldValue("changeAuthor");
-
- EmailMessage message = new EmailMessage()
- .setMessageId("issue-changes/" + issueKey)
- .setSubject(projectName + ", change on issue #" + issueKey)
- .setMessage(sb.toString());
- if (author != null) {
- message.setFrom(getUserFullName(author));
+ /**
+ * Adds "projectName" or "projectName, branchName" if branchName is non null
+ */
+ protected static void toString(StringBuilder sb, Project project) {
+ Optional<String> branchName = project.getBranchName();
+ if (branchName.isPresent()) {
+ sb.append(project.getProjectName()).append(", ").append(branchName.get());
+ } else {
+ sb.append(project.getProjectName());
}
- return message;
}
- private static void appendChanges(Notification notif, StringBuilder sb) {
- appendField(sb, "Comment", null, notif.getFieldValue("comment"));
- appendFieldWithoutHistory(sb, "Assignee", notif.getFieldValue("old.assignee"), notif.getFieldValue("new.assignee"));
- appendField(sb, "Severity", notif.getFieldValue("old.severity"), notif.getFieldValue("new.severity"));
- appendField(sb, "Type", notif.getFieldValue("old.type"), notif.getFieldValue("new.type"));
- appendField(sb, "Resolution", notif.getFieldValue("old.resolution"), notif.getFieldValue("new.resolution"));
- appendField(sb, "Status", notif.getFieldValue("old.status"), notif.getFieldValue("new.status"));
- appendField(sb, "Message", notif.getFieldValue("old.message"), notif.getFieldValue("new.message"));
- appendField(sb, "Author", notif.getFieldValue("old.author"), notif.getFieldValue("new.author"));
- appendFieldWithoutHistory(sb, "Action Plan", notif.getFieldValue("old.actionPlan"), notif.getFieldValue("new.actionPlan"));
- appendField(sb, "Tags", formatTagChange(notif.getFieldValue("old.tags")), formatTagChange(notif.getFieldValue("new.tags")));
+ static String toUrlParams(Project project) {
+ return "id=" + urlEncode(project.getKey()) +
+ project.getBranchName().map(branchName -> "&branch=" + urlEncode(branchName)).orElse("");
}
- @CheckForNull
- private static String formatTagChange(@Nullable String tags) {
- if (tags == null) {
- return null;
- } else {
- return "[" + tags + "]";
- }
+ void addIssuesByProjectThenRule(StringBuilder sb, SetMultimap<Project, ChangedIssue> issuesByProject) {
+ issuesByProject.keySet().stream()
+ .sorted(PROJECT_COMPARATOR)
+ .forEach(project -> {
+ String encodedProjectParams = toUrlParams(project);
+ paragraph(sb, s -> toString(s, project));
+ addIssuesByRule(sb, issuesByProject.get(project), projectIssuePageHref(encodedProjectParams));
+ });
}
- private static void appendHeader(Notification notif, StringBuilder sb) {
- appendLine(sb, StringUtils.defaultString(notif.getFieldValue("componentName"), notif.getFieldValue("componentKey")));
- String branchName = notif.getFieldValue(FIELD_BRANCH);
- if (branchName != null) {
- appendField(sb, "Branch", null, branchName);
- }
- String pullRequest = notif.getFieldValue(FIELD_PULL_REQUEST);
- if (pullRequest != null) {
- appendField(sb, "Pull request", null, pullRequest);
+ void addIssuesByRule(StringBuilder sb, Collection<ChangedIssue> changedIssues, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref) {
+ ListMultimap<Rule, ChangedIssue> issuesByRule = changedIssues.stream()
+ .collect(index(ChangedIssue::getRule, t -> t));
+
+ Iterator<Rule> rules = issuesByRule.keySet().stream()
+ .sorted(RULE_COMPARATOR)
+ .iterator();
+ if (!rules.hasNext()) {
+ return;
}
- appendField(sb, "Rule", null, notif.getFieldValue("ruleName"));
- appendField(sb, "Message", null, notif.getFieldValue("message"));
- }
- private void appendFooter(StringBuilder sb, Notification notification) {
- String issueKey = notification.getFieldValue("key");
- try {
- sb.append("More details at: ").append(settings.getServerBaseURL())
- .append("/project/issues?id=").append(encode(notification.getFieldValue("projectKey"), "UTF-8"))
- .append("&issues=").append(issueKey)
- .append("&open=").append(issueKey);
- String branchName = notification.getFieldValue(FIELD_BRANCH);
- if (branchName != null) {
- sb.append("&branch=").append(branchName);
- }
- String pullRequest = notification.getFieldValue(FIELD_PULL_REQUEST);
- if (pullRequest != null) {
- sb.append("&pullRequest=").append(pullRequest);
- }
- sb.append(NEW_LINE);
- } catch (UnsupportedEncodingException e) {
- throw new IllegalStateException("Encoding not supported", e);
+ sb.append("<ul>");
+ while (rules.hasNext()) {
+ Rule rule = rules.next();
+ Collection<ChangedIssue> issues = issuesByRule.get(rule);
+
+ sb.append("<li>").append("Rule ").append(" <em>").append(rule.getName()).append("</em> - ");
+ appendIssueLinks(sb, issuePageHref, issues);
+ sb.append("</li>");
}
+ sb.append("</ul>");
}
- private static void appendLine(StringBuilder sb, @Nullable String line) {
- if (!Strings.isNullOrEmpty(line)) {
- sb.append(line).append(NEW_LINE);
+ private static void appendIssueLinks(StringBuilder sb, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref, Collection<ChangedIssue> issues) {
+ SortedSet<ChangedIssue> sortedIssues = ImmutableSortedSet.copyOf(CHANGED_ISSUE_KEY_COMPARATOR, issues);
+ int issueCount = issues.size();
+ if (issueCount == 1) {
+ link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See the single issue"));
+ } else if (issueCount <= MAX_ISSUES_BY_LINK) {
+ link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See all ").append(issueCount).append(" issues"));
+ } else {
+ sb.append("See issues");
+ List<List<ChangedIssue>> issueGroups = Lists.partition(ImmutableList.copyOf(sortedIssues), MAX_ISSUES_BY_LINK);
+ Iterator<List<ChangedIssue>> issueGroupsIterator = issueGroups.iterator();
+ int[] groupIndex = new int[] {0};
+ while (issueGroupsIterator.hasNext()) {
+ List<ChangedIssue> issueGroup = issueGroupsIterator.next();
+ sb.append(' ');
+ link(sb, s -> issuePageHref.accept(s, issueGroup), issueGroupLabel(sb, groupIndex, issueGroup));
+ groupIndex[0]++;
+ }
}
}
- private static void appendField(StringBuilder sb, String name, @Nullable String oldValue, @Nullable String newValue) {
- if (oldValue != null || newValue != null) {
- sb.append(name).append(": ");
- if (newValue != null) {
- sb.append(newValue);
+ BiConsumer<StringBuilder, Collection<ChangedIssue>> projectIssuePageHref(String projectParams) {
+ return (s, issues) -> {
+ s.append(settings.getServerBaseURL()).append("/project/issues?").append(projectParams)
+ .append("&issues=");
+
+ Iterator<ChangedIssue> issueIterator = issues.iterator();
+ while (issueIterator.hasNext()) {
+ s.append(urlEncode(issueIterator.next().getKey()));
+ if (issueIterator.hasNext()) {
+ s.append(URL_ENCODED_COMMA);
+ }
}
- if (oldValue != null) {
- sb.append(" (was ").append(oldValue).append(")");
+
+ if (issues.size() == 1) {
+ s.append("&open=").append(urlEncode(issues.iterator().next().getKey()));
}
- sb.append(NEW_LINE);
- }
+ };
}
- private static void appendFieldWithoutHistory(StringBuilder sb, String name, @Nullable String oldValue, @Nullable String newValue) {
- if (oldValue != null || newValue != null) {
- sb.append(name);
- if (newValue != null) {
- sb.append(" changed to ");
- sb.append(newValue);
+ private static Consumer<StringBuilder> issueGroupLabel(StringBuilder sb, int[] groupIndex, List<ChangedIssue> issueGroup) {
+ return s -> {
+ int firstIssueNumber = (groupIndex[0] * MAX_ISSUES_BY_LINK) + 1;
+ if (issueGroup.size() == 1) {
+ sb.append(firstIssueNumber);
} else {
- sb.append(" removed");
+ sb.append(firstIssueNumber).append("-").append(firstIssueNumber + issueGroup.size() - 1);
}
- sb.append(NEW_LINE);
- }
+ };
}
- private String getUserFullName(@Nullable String login) {
- if (login == null) {
- return null;
- }
- try (DbSession dbSession = dbClient.openSession(false)) {
- UserDto userDto = dbClient.userDao().selectByLogin(dbSession, login);
- if (userDto == null || !userDto.isActive()) {
- // most probably user was deleted
- return login;
- }
- return StringUtils.defaultIfBlank(userDto.getName(), login);
+ void addFooter(StringBuilder sb, String notificationI18nKey) {
+ paragraph(sb, s -> s.append("&nbsp;"));
+ paragraph(sb, s -> {
+ s.append("<small>");
+ s.append("You received this email because you are subscribed to ")
+ .append('"').append(i18n.message(Locale.ENGLISH, notificationI18nKey, notificationI18nKey)).append('"')
+ .append(" notifications from ").append(settings.getInstanceName()).append(".");
+ s.append(" Click ");
+ link(s, s1 -> s1.append(settings.getServerBaseURL()).append("/account/notifications"), s1 -> s1.append("here"));
+ s.append(" to edit your email preferences.");
+ s.append("</small>");
+ });
+ }
+
+ protected static void paragraph(StringBuilder sb, Consumer<StringBuilder> content) {
+ sb.append("<p>");
+ content.accept(sb);
+ sb.append("</p>");
+ }
+
+ protected static void link(StringBuilder sb, Consumer<StringBuilder> link, Consumer<StringBuilder> content) {
+ sb.append("<a href=\"");
+ link.accept(sb);
+ sb.append("\">");
+ content.accept(sb);
+ sb.append("</a>");
+ }
+
+ private static String urlEncode(String str) {
+ try {
+ return encode(str, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException(e);
}
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotification.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotification.java
new file mode 100644
index 00000000000..2a7060d58a1
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotification.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import org.sonar.api.notifications.Notification;
+
+public class IssuesChangesNotification extends Notification {
+
+ public static final String TYPE = "issues-changes";
+
+ public IssuesChangesNotification() {
+ super(TYPE);
+ }
+
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java
new file mode 100644
index 00000000000..a0fadc8fd92
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java
@@ -0,0 +1,461 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.rule.RuleKey;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+
+@Immutable
+public class IssuesChangesNotificationBuilder {
+
+ private static final String KEY_CANT_BE_NULL_MESSAGE = "key can't be null";
+ private final Set<ChangedIssue> issues;
+ private final Change change;
+
+ public IssuesChangesNotificationBuilder(Set<ChangedIssue> issues, Change change) {
+ checkArgument(!issues.isEmpty(), "issues can't be empty");
+
+ this.issues = ImmutableSet.copyOf(issues);
+ this.change = requireNonNull(change, "change can't be null");
+ }
+
+ public Set<ChangedIssue> getIssues() {
+ return issues;
+ }
+
+ public Change getChange() {
+ return change;
+ }
+
+ @Immutable
+ public static final class ChangedIssue {
+ private final String key;
+ private final String newStatus;
+ @CheckForNull
+ private final String newResolution;
+ @CheckForNull
+ private final User assignee;
+ private final Rule rule;
+ private final Project project;
+
+ public ChangedIssue(Builder builder) {
+ this.key = requireNonNull(builder.key, KEY_CANT_BE_NULL_MESSAGE);
+ this.newStatus = requireNonNull(builder.newStatus, "newStatus can't be null");
+ this.newResolution = builder.newResolution;
+ this.assignee = builder.assignee;
+ this.rule = requireNonNull(builder.rule, "rule can't be null");
+ this.project = requireNonNull(builder.project, "project can't be null");
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getNewStatus() {
+ return newStatus;
+ }
+
+ public Optional<String> getNewResolution() {
+ return ofNullable(newResolution);
+ }
+
+ public Optional<User> getAssignee() {
+ return ofNullable(assignee);
+ }
+
+ public Rule getRule() {
+ return rule;
+ }
+
+ public Project getProject() {
+ return project;
+ }
+
+ public static class Builder {
+ private final String key;
+ private String newStatus;
+ @CheckForNull
+ private String newResolution;
+ @CheckForNull
+ private User assignee;
+ private Rule rule;
+ private Project project;
+
+ public Builder(String key) {
+ this.key = key;
+ }
+
+ public Builder setNewStatus(String newStatus) {
+ this.newStatus = newStatus;
+ return this;
+ }
+
+ public Builder setNewResolution(@Nullable String newResolution) {
+ this.newResolution = newResolution;
+ return this;
+ }
+
+ public Builder setAssignee(@Nullable User assignee) {
+ this.assignee = assignee;
+ return this;
+ }
+
+ public Builder setRule(Rule rule) {
+ this.rule = rule;
+ return this;
+ }
+
+ public Builder setProject(Project project) {
+ this.project = project;
+ return this;
+ }
+
+ public ChangedIssue build() {
+ return new ChangedIssue(this);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ChangedIssue that = (ChangedIssue) o;
+ return key.equals(that.key) &&
+ newStatus.equals(that.newStatus) &&
+ Objects.equals(newResolution, that.newResolution) &&
+ Objects.equals(assignee, that.assignee) &&
+ rule.equals(that.rule) &&
+ project.equals(that.project);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, newStatus, newResolution, assignee, rule, project);
+ }
+
+ @Override
+ public String toString() {
+ return "ChangedIssue{" +
+ "key='" + key + '\'' +
+ ", newStatus='" + newStatus + '\'' +
+ ", newResolution='" + newResolution + '\'' +
+ ", assignee=" + assignee +
+ ", rule=" + rule +
+ ", project=" + project +
+ '}';
+ }
+ }
+
+ public static final class User {
+ private final String uuid;
+ private final String login;
+ @CheckForNull
+ private final String name;
+
+ public User(String uuid, String login, @Nullable String name) {
+ this.uuid = requireNonNull(uuid, "uuid can't be null");
+ this.login = requireNonNull(login, "login can't be null");
+ this.name = name;
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public Optional<String> getName() {
+ return ofNullable(name);
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "uuid='" + uuid + '\'' +
+ ", login='" + login + '\'' +
+ ", name='" + name + '\'' +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ User user = (User) o;
+ return uuid.equals(user.uuid) &&
+ login.equals(user.login) &&
+ Objects.equals(name, user.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uuid, login, name);
+ }
+ }
+
+ @Immutable
+ public static final class Rule {
+ private final RuleKey key;
+ private final String name;
+
+ public Rule(RuleKey key, String name) {
+ this.key = requireNonNull(key, KEY_CANT_BE_NULL_MESSAGE);
+ this.name = requireNonNull(name, "name can't be null");
+ }
+
+ public RuleKey getKey() {
+ return key;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Rule that = (Rule) o;
+ return key.equals(that.key) && name.equals(that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, name);
+ }
+
+ @Override
+ public String toString() {
+ return "Rule{" +
+ "key=" + key +
+ ", name='" + name + '\'' +
+ '}';
+ }
+ }
+
+ @Immutable
+ public static final class Project {
+ private final String uuid;
+ private final String key;
+ private final String projectName;
+ @Nullable
+ private final String branchName;
+
+ public Project(Builder builder) {
+ this.uuid = requireNonNull(builder.uuid, "uuid can't be null");
+ this.key = requireNonNull(builder.key, KEY_CANT_BE_NULL_MESSAGE);
+ this.projectName = requireNonNull(builder.projectName, "projectName can't be null");
+ this.branchName = builder.branchName;
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getProjectName() {
+ return projectName;
+ }
+
+ public Optional<String> getBranchName() {
+ return ofNullable(branchName);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Project project = (Project) o;
+ return uuid.equals(project.uuid) &&
+ key.equals(project.key) &&
+ projectName.equals(project.projectName) &&
+ Objects.equals(branchName, project.branchName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uuid, key, projectName, branchName);
+ }
+
+ @Override
+ public String toString() {
+ return "Project{" +
+ "uuid='" + uuid + '\'' +
+ ", key='" + key + '\'' +
+ ", projectName='" + projectName + '\'' +
+ ", branchName='" + branchName + '\'' +
+ '}';
+ }
+
+ public static class Builder {
+ private final String uuid;
+ private String key;
+ private String projectName;
+ @CheckForNull
+ private String branchName;
+
+ public Builder(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public Builder setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ public Builder setProjectName(String projectName) {
+ this.projectName = projectName;
+ return this;
+ }
+
+ public Builder setBranchName(@Nullable String branchName) {
+ this.branchName = branchName;
+ return this;
+ }
+
+ public Project build() {
+ return new Project(this);
+ }
+ }
+ }
+
+ public abstract static class Change {
+ protected final long date;
+
+ private Change(long date) {
+ this.date = requireNonNull(date, "date can't be null");
+ }
+
+ public long getDate() {
+ return date;
+ }
+
+ public abstract boolean isAuthorLogin(String login);
+ }
+
+ @Immutable
+ public static final class AnalysisChange extends Change {
+ public AnalysisChange(long date) {
+ super(date);
+ }
+
+ @Override
+ public boolean isAuthorLogin(String login) {
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Change change = (Change) o;
+ return date == change.date;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(date);
+ }
+
+ @Override
+ public String toString() {
+ return "AnalysisChange{" + date + '}';
+ }
+ }
+
+ @Immutable
+ public static final class UserChange extends Change {
+ private final User user;
+
+ public UserChange(long date, User user) {
+ super(date);
+ this.user = requireNonNull(user, "user can't be null");
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ @Override
+ public boolean isAuthorLogin(String login) {
+ return this.user.login.equals(login);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ UserChange that = (UserChange) o;
+ return date == that.date && user.equals(that.user);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(user, date);
+ }
+
+ @Override
+ public String toString() {
+ return "UserChange{" +
+ "date=" + date +
+ ", user=" + user +
+ '}';
+ }
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationModule.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationModule.java
new file mode 100644
index 00000000000..5d1efab8650
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationModule.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import org.sonar.core.platform.Module;
+
+public class IssuesChangesNotificationModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ ChangesOnMyIssueNotificationHandler.class,
+ ChangesOnMyIssueNotificationHandler.newMetadata(),
+ ChangesOnMyIssuesEmailTemplate.class,
+ FPOrWontFixNotificationHandler.class,
+ FPOrWontFixNotificationHandler.newMetadata(),
+ IssuesChangesNotificationSerializer.class,
+ FpOrWontFixEmailTemplate.class
+ );
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java
new file mode 100644
index 00000000000..42982326eb9
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java
@@ -0,0 +1,307 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static org.sonar.core.util.stream.MoreCollectors.toSet;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+public class IssuesChangesNotificationSerializer {
+ private static final String FIELD_ISSUES_COUNT = "issues.count";
+ private static final String FIELD_CHANGE_DATE = "change.date";
+ private static final String FIELD_CHANGE_AUTHOR_UUID = "change.author.uuid";
+ private static final String FIELD_CHANGE_AUTHOR_LOGIN = "change.author.login";
+ private static final String FIELD_CHANGE_AUTHOR_NAME = "change.author.name";
+
+ public IssuesChangesNotification serialize(IssuesChangesNotificationBuilder builder) {
+ IssuesChangesNotification res = new IssuesChangesNotification();
+ serializeIssueSize(res, builder.getIssues());
+ serializeChange(res, builder.getChange());
+ serializeIssues(res, builder.getIssues());
+ serializeRules(res, builder.getIssues());
+ serializeProjects(res, builder.getIssues());
+
+ return res;
+ }
+
+ /**
+ * @throws IllegalArgumentException if {@code notification} misses any field or of any has unsupported value
+ */
+ public IssuesChangesNotificationBuilder from(IssuesChangesNotification notification) {
+ int issueCount = readIssueCount(notification);
+ IssuesChangesNotificationBuilder.Change change = readChange(notification);
+ List<Issue> issues = readIssues(notification, issueCount);
+ Map<String, Project> projects = readProjects(notification, issues);
+ Map<RuleKey, Rule> rules = readRules(notification, issues);
+
+ return new IssuesChangesNotificationBuilder(buildChangedIssues(issues, projects, rules), change);
+ }
+
+ private static void serializeIssueSize(IssuesChangesNotification res, Set<ChangedIssue> issues) {
+ res.setFieldValue(FIELD_ISSUES_COUNT, String.valueOf(issues.size()));
+ }
+
+ private static int readIssueCount(IssuesChangesNotification notification) {
+ String fieldValue = notification.getFieldValue(FIELD_ISSUES_COUNT);
+ checkArgument(fieldValue != null, "missing field %s", FIELD_ISSUES_COUNT);
+ int issueCount = Integer.parseInt(fieldValue);
+ checkArgument(issueCount > 0, "issue count must be >= 1");
+ return issueCount;
+ }
+
+ private static Set<ChangedIssue> buildChangedIssues(List<Issue> issues, Map<String, Project> projects,
+ Map<RuleKey, Rule> rules) {
+ return issues.stream()
+ .map(issue -> new ChangedIssue.Builder(issue.key)
+ .setNewStatus(issue.newStatus)
+ .setNewResolution(issue.newResolution)
+ .setAssignee(issue.assignee)
+ .setRule(rules.get(issue.ruleKey))
+ .setProject(projects.get(issue.projectUuid))
+ .build())
+ .collect(toSet(issues.size()));
+ }
+
+ private static void serializeIssues(IssuesChangesNotification res, Set<ChangedIssue> issues) {
+ int index = 0;
+ for (ChangedIssue issue : issues) {
+ serializeIssue(res, index, issue);
+ index++;
+ }
+ }
+
+ private static List<Issue> readIssues(IssuesChangesNotification notification, int issueCount) {
+ List<Issue> res = new ArrayList<>(issueCount);
+ for (int i = 0; i < issueCount; i++) {
+ res.add(readIssue(notification, i));
+ }
+ return res;
+ }
+
+ private static void serializeIssue(IssuesChangesNotification notification, int index, ChangedIssue issue) {
+ String issuePropertyPrefix = "issues." + index;
+ notification.setFieldValue(issuePropertyPrefix + ".key", issue.getKey());
+ issue.getAssignee()
+ .ifPresent(assignee -> {
+ notification.setFieldValue(issuePropertyPrefix + ".assignee.uuid", assignee.getUuid());
+ notification.setFieldValue(issuePropertyPrefix + ".assignee.login", assignee.getLogin());
+ assignee.getName()
+ .ifPresent(name -> notification.setFieldValue(issuePropertyPrefix + ".assignee.name", name));
+ });
+ issue.getNewResolution()
+ .ifPresent(newResolution -> notification.setFieldValue(issuePropertyPrefix + ".newResolution", newResolution));
+ notification.setFieldValue(issuePropertyPrefix + ".newStatus", issue.getNewStatus());
+ notification.setFieldValue(issuePropertyPrefix + ".ruleKey", issue.getRule().getKey().toString());
+ notification.setFieldValue(issuePropertyPrefix + ".projectUuid", issue.getProject().getUuid());
+ }
+
+ private static Issue readIssue(IssuesChangesNotification notification, int index) {
+ String issuePropertyPrefix = "issues." + index;
+ User assignee = readAssignee(notification, issuePropertyPrefix, index);
+ return new Issue.Builder()
+ .setKey(getIssueFieldValue(notification, issuePropertyPrefix + ".key", index))
+ .setNewStatus(getIssueFieldValue(notification, issuePropertyPrefix + ".newStatus", index))
+ .setNewResolution(notification.getFieldValue(issuePropertyPrefix + ".newResolution"))
+ .setAssignee(assignee)
+ .setRuleKey(getIssueFieldValue(notification, issuePropertyPrefix + ".ruleKey", index))
+ .setProjectUuid(getIssueFieldValue(notification, issuePropertyPrefix + ".projectUuid", index))
+ .build();
+ }
+
+ @CheckForNull
+ private static User readAssignee(IssuesChangesNotification notification, String issuePropertyPrefix, int index) {
+ String uuid = notification.getFieldValue(issuePropertyPrefix + ".assignee.uuid");
+ if (uuid == null) {
+ return null;
+ }
+ String login = getIssueFieldValue(notification, issuePropertyPrefix + ".assignee.login", index);
+ return new User(uuid, login, notification.getFieldValue(issuePropertyPrefix + ".assignee.name"));
+ }
+
+ private static String getIssueFieldValue(IssuesChangesNotification notification, String fieldName, int index) {
+ String fieldValue = notification.getFieldValue(fieldName);
+ checkState(fieldValue != null, "Can not find field %s for issue with index %s", fieldName, index);
+ return fieldValue;
+ }
+
+ private static void serializeRules(IssuesChangesNotification res, Set<ChangedIssue> issues) {
+ issues.stream()
+ .map(ChangedIssue::getRule)
+ .collect(Collectors.toSet())
+ .forEach(rule -> res.setFieldValue("rules." + rule.getKey(), rule.getName()));
+ }
+
+ private static Map<RuleKey, Rule> readRules(IssuesChangesNotification notification, List<Issue> issues) {
+ return issues.stream()
+ .map(issue -> issue.ruleKey)
+ .collect(Collectors.toSet())
+ .stream()
+ .map(ruleKey -> readRule(notification, ruleKey))
+ .collect(uniqueIndex(Rule::getKey, t -> t));
+ }
+
+ private static Rule readRule(IssuesChangesNotification notification, RuleKey ruleKey) {
+ String fieldName = "rules." + ruleKey;
+ String ruleName = notification.getFieldValue(fieldName);
+ checkState(ruleName != null, "can not find field %s", ruleKey);
+ return new Rule(ruleKey, ruleName);
+ }
+
+ private static void serializeProjects(IssuesChangesNotification res, Set<ChangedIssue> issues) {
+ issues.stream()
+ .map(ChangedIssue::getProject)
+ .collect(Collectors.toSet())
+ .forEach(project -> {
+ String projectPropertyPrefix = "projects." + project.getUuid();
+ res.setFieldValue(projectPropertyPrefix + ".key", project.getKey());
+ res.setFieldValue(projectPropertyPrefix + ".projectName", project.getProjectName());
+ project.getBranchName()
+ .ifPresent(branchName -> res.setFieldValue(projectPropertyPrefix + ".branchName", branchName));
+ });
+ }
+
+ private static Map<String, Project> readProjects(IssuesChangesNotification notification, List<Issue> issues) {
+ return issues.stream()
+ .map(issue -> issue.projectUuid)
+ .collect(Collectors.toSet())
+ .stream()
+ .map(projectUuid -> {
+ String projectPropertyPrefix = "projects." + projectUuid;
+ return new Project.Builder(projectUuid)
+ .setKey(getProjectFieldValue(notification, projectPropertyPrefix + ".key", projectUuid))
+ .setProjectName(getProjectFieldValue(notification, projectPropertyPrefix + ".projectName", projectUuid))
+ .setBranchName(notification.getFieldValue(projectPropertyPrefix + ".branchName"))
+ .build();
+ })
+ .collect(uniqueIndex(Project::getUuid, t -> t));
+ }
+
+ private static String getProjectFieldValue(IssuesChangesNotification notification, String fieldName, String uuid) {
+ String fieldValue = notification.getFieldValue(fieldName);
+ checkState(fieldValue != null, "Can not find field %s for project with uuid %s", fieldName, uuid);
+ return fieldValue;
+ }
+
+ private static void serializeChange(IssuesChangesNotification notification, IssuesChangesNotificationBuilder.Change change) {
+ notification.setFieldValue(FIELD_CHANGE_DATE, String.valueOf(change.date));
+ if (change instanceof IssuesChangesNotificationBuilder.UserChange) {
+ IssuesChangesNotificationBuilder.UserChange userChange = (IssuesChangesNotificationBuilder.UserChange) change;
+ User user = userChange.getUser();
+ notification.setFieldValue(FIELD_CHANGE_AUTHOR_UUID, user.getUuid());
+ notification.setFieldValue(FIELD_CHANGE_AUTHOR_LOGIN, user.getLogin());
+ user.getName().ifPresent(name -> notification.setFieldValue(FIELD_CHANGE_AUTHOR_NAME, name));
+ }
+ }
+
+ private static IssuesChangesNotificationBuilder.Change readChange(IssuesChangesNotification notification) {
+ String dateFieldValue = notification.getFieldValue(FIELD_CHANGE_DATE);
+ checkState(dateFieldValue != null, "Can not find field %s", FIELD_CHANGE_DATE);
+ long date = Long.parseLong(dateFieldValue);
+
+ String uuid = notification.getFieldValue(FIELD_CHANGE_AUTHOR_UUID);
+ if (uuid == null) {
+ return new IssuesChangesNotificationBuilder.AnalysisChange(date);
+ }
+ String login = notification.getFieldValue(FIELD_CHANGE_AUTHOR_LOGIN);
+ checkState(login != null, "Can not find field %s", FIELD_CHANGE_AUTHOR_LOGIN);
+ return new IssuesChangesNotificationBuilder.UserChange(date, new User(uuid, login, notification.getFieldValue(FIELD_CHANGE_AUTHOR_NAME)));
+ }
+
+ @Immutable
+ private static final class Issue {
+ private final String key;
+ private final String newStatus;
+ @CheckForNull
+ private final String newResolution;
+ @CheckForNull
+ private final User assignee;
+ private final RuleKey ruleKey;
+ private final String projectUuid;
+
+ private Issue(Builder builder) {
+ this.key = builder.key;
+ this.newResolution = builder.newResolution;
+ this.newStatus = builder.newStatus;
+ this.assignee = builder.assignee;
+ this.ruleKey = RuleKey.parse(builder.ruleKey);
+ this.projectUuid = builder.projectUuid;
+ }
+
+ static class Builder {
+ private String key = null;
+ private String newStatus = null;
+ @CheckForNull
+ private String newResolution = null;
+ @CheckForNull
+ private User assignee = null;
+ private String ruleKey = null;
+ private String projectUuid = null;
+
+ public Builder setKey(String key) {
+ this.key = key;
+ return this;
+ }
+
+ public Builder setNewStatus(String newStatus) {
+ this.newStatus = newStatus;
+ return this;
+ }
+
+ public Builder setNewResolution(@Nullable String newResolution) {
+ this.newResolution = newResolution;
+ return this;
+ }
+
+ public Builder setAssignee(@Nullable User assignee) {
+ this.assignee = assignee;
+ return this;
+ }
+
+ public Builder setRuleKey(String ruleKey) {
+ this.ruleKey = ruleKey;
+ return this;
+ }
+
+ public Builder setProjectUuid(String projectUuid) {
+ this.projectUuid = projectUuid;
+ return this;
+ }
+
+ public Issue build() {
+ return new Issue(this);
+ }
+ }
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NotificationWithProjectKeys.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NotificationWithProjectKeys.java
new file mode 100644
index 00000000000..ad004aa127a
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NotificationWithProjectKeys.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+
+final class NotificationWithProjectKeys {
+ private final IssuesChangesNotificationBuilder builder;
+ private final Set<String> projectKeys;
+
+ protected NotificationWithProjectKeys(IssuesChangesNotificationBuilder builder) {
+ this.builder = builder;
+ this.projectKeys = builder.getIssues().stream().map(t -> t.getProject().getKey()).collect(Collectors.toSet());
+ }
+
+ public Set<ChangedIssue> getIssues() {
+ return builder.getIssues();
+ }
+
+ public Change getChange() {
+ return builder.getChange();
+ }
+
+ public Set<String> getProjectKeys() {
+ return projectKeys;
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java b/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java
index 224164d68d0..0f4090daba3 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java
@@ -26,7 +26,9 @@ import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.concurrent.Immutable;
import org.apache.commons.lang.StringUtils;
+import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
+import org.apache.commons.mail.HtmlEmail;
import org.apache.commons.mail.SimpleEmail;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification;
@@ -227,46 +229,13 @@ public class EmailNotificationChannel extends NotificationChannel {
try {
LOG.trace("Sending email: {}", emailMessage);
- String host = null;
- try {
- host = new URL(configuration.getServerBaseURL()).getHost();
- } catch (MalformedURLException e) {
- // ignore
- }
+ String host = resolveHost();
- SimpleEmail email = new SimpleEmail();
- if (StringUtils.isNotBlank(host)) {
- /*
- * Set headers for proper threading: GMail will not group messages, even if they have same subject, but don't have "In-Reply-To" and
- * "References" headers. TODO investigate threading in other clients like KMail, Thunderbird, Outlook
- */
- if (StringUtils.isNotEmpty(emailMessage.getMessageId())) {
- String messageId = "<" + emailMessage.getMessageId() + "@" + host + ">";
- email.addHeader(IN_REPLY_TO_HEADER, messageId);
- email.addHeader(REFERENCES_HEADER, messageId);
- }
- // Set headers for proper filtering
- email.addHeader(LIST_ID_HEADER, "SonarQube <sonar." + host + ">");
- email.addHeader(LIST_ARCHIVE_HEADER, configuration.getServerBaseURL());
- }
- // Set general information
- email.setCharset("UTF-8");
- String fromName = configuration.getFromName();
- String from = StringUtils.isBlank(emailMessage.getFrom()) ? fromName : (emailMessage.getFrom() + " (" + fromName + ")");
- email.setFrom(configuration.getFrom(), from);
- email.addTo(emailMessage.getTo(), " ");
- String subject = StringUtils.defaultIfBlank(StringUtils.trimToEmpty(configuration.getPrefix()) + " ", "")
- + StringUtils.defaultString(emailMessage.getSubject(), SUBJECT_DEFAULT);
- email.setSubject(subject);
- email.setMsg(emailMessage.getMessage());
- // Send
- email.setHostName(configuration.getSmtpHost());
- configureSecureConnection(email);
- if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) {
- email.setAuthentication(configuration.getSmtpUsername(), configuration.getSmtpPassword());
- }
- email.setSocketConnectionTimeout(SOCKET_TIMEOUT);
- email.setSocketTimeout(SOCKET_TIMEOUT);
+ Email email = createEmailWithMessage(emailMessage);
+ setHeaders(email, emailMessage, host);
+ setConnectionDetails(email);
+ setToAndFrom(email, emailMessage);
+ setSubject(email, emailMessage);
email.send();
} finally {
@@ -274,7 +243,66 @@ public class EmailNotificationChannel extends NotificationChannel {
}
}
- private void configureSecureConnection(SimpleEmail email) {
+ private static Email createEmailWithMessage(EmailMessage emailMessage) throws EmailException {
+ if (emailMessage.isHtml()) {
+ return new HtmlEmail().setHtmlMsg(emailMessage.getMessage());
+ }
+ return new SimpleEmail().setMsg(emailMessage.getMessage());
+ }
+
+ private void setSubject(Email email, EmailMessage emailMessage) {
+ String subject = StringUtils.defaultIfBlank(StringUtils.trimToEmpty(configuration.getPrefix()) + " ", "")
+ + StringUtils.defaultString(emailMessage.getSubject(), SUBJECT_DEFAULT);
+ email.setSubject(subject);
+ }
+
+ private void setToAndFrom(Email email, EmailMessage emailMessage) throws EmailException {
+ String fromName = configuration.getFromName();
+ String from = StringUtils.isBlank(emailMessage.getFrom()) ? fromName : (emailMessage.getFrom() + " (" + fromName + ")");
+ email.setFrom(configuration.getFrom(), from);
+ email.addTo(emailMessage.getTo(), " ");
+ }
+
+ @CheckForNull
+ private String resolveHost() {
+ try {
+ return new URL(configuration.getServerBaseURL()).getHost();
+ } catch (MalformedURLException e) {
+ // ignore
+ return null;
+ }
+ }
+
+ private void setHeaders(Email email, EmailMessage emailMessage, @CheckForNull String host) {
+ // Set general information
+ email.setCharset("UTF-8");
+ if (StringUtils.isNotBlank(host)) {
+ /*
+ * Set headers for proper threading: GMail will not group messages, even if they have same subject, but don't have "In-Reply-To" and
+ * "References" headers. TODO investigate threading in other clients like KMail, Thunderbird, Outlook
+ */
+ if (StringUtils.isNotEmpty(emailMessage.getMessageId())) {
+ String messageId = "<" + emailMessage.getMessageId() + "@" + host + ">";
+ email.addHeader(IN_REPLY_TO_HEADER, messageId);
+ email.addHeader(REFERENCES_HEADER, messageId);
+ }
+ // Set headers for proper filtering
+ email.addHeader(LIST_ID_HEADER, "SonarQube <sonar." + host + ">");
+ email.addHeader(LIST_ARCHIVE_HEADER, configuration.getServerBaseURL());
+ }
+ }
+
+ private void setConnectionDetails(Email email) {
+ email.setHostName(configuration.getSmtpHost());
+ configureSecureConnection(email);
+ if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) {
+ email.setAuthentication(configuration.getSmtpUsername(), configuration.getSmtpPassword());
+ }
+ email.setSocketConnectionTimeout(SOCKET_TIMEOUT);
+ email.setSocketTimeout(SOCKET_TIMEOUT);
+ }
+
+ private void configureSecureConnection(Email email) {
if (StringUtils.equalsIgnoreCase(configuration.getSecureConnection(), "ssl")) {
email.setSSLOnConnect(true);
email.setSSLCheckServerIdentity(true);
@@ -305,7 +333,7 @@ public class EmailNotificationChannel extends NotificationChannel {
EmailMessage emailMessage = new EmailMessage();
emailMessage.setTo(toAddress);
emailMessage.setSubject(subject);
- emailMessage.setMessage(message);
+ emailMessage.setPlainTextMessage(message);
send(emailMessage);
} catch (EmailException e) {
LOG.debug("Fail to send test email to {}: {}", toAddress, e);
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/notification/QGChangeEmailTemplate.java b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/notification/QGChangeEmailTemplate.java
index d5a883e1d66..9da89355546 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/notification/QGChangeEmailTemplate.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/notification/QGChangeEmailTemplate.java
@@ -19,6 +19,7 @@
*/
package org.sonar.server.qualitygate.notification;
+import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.config.EmailSettings;
@@ -41,6 +42,7 @@ public class QGChangeEmailTemplate implements EmailTemplate {
}
@Override
+ @CheckForNull
public EmailMessage format(Notification notification) {
if (!"alerts".equals(notification.getType())) {
return null;
@@ -66,7 +68,7 @@ public class QGChangeEmailTemplate implements EmailTemplate {
return new EmailMessage()
.setMessageId("alerts/" + projectId)
.setSubject(subject)
- .setMessage(messageBody);
+ .setPlainTextMessage(messageBody);
}
private static String computeFullProjectName(String projectName, @Nullable String branchName) {
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandlerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandlerTest.java
index b6fae117865..9f51b5b233b 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandlerTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandlerTest.java
@@ -20,10 +20,12 @@
package org.sonar.server.issue.notification;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Collections;
+import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
@@ -32,22 +34,33 @@ import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
import org.sonar.server.notification.NotificationDispatcherMetadata;
import org.sonar.server.notification.NotificationManager;
import org.sonar.server.notification.email.EmailNotificationChannel;
import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
-import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
+import static org.sonar.core.util.stream.MoreCollectors.index;
+import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex;
import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
@@ -59,7 +72,12 @@ public class ChangesOnMyIssueNotificationHandlerTest {
private NotificationManager notificationManager = mock(NotificationManager.class);
private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
- private ChangesOnMyIssueNotificationHandler underTest = new ChangesOnMyIssueNotificationHandler(notificationManager, emailNotificationChannel);
+ private IssuesChangesNotificationSerializer serializer = new IssuesChangesNotificationSerializer();
+ private ChangesOnMyIssueNotificationHandler underTest = new ChangesOnMyIssueNotificationHandler(
+ notificationManager, emailNotificationChannel, serializer);
+
+ private Class<Set<EmailDeliveryRequest>> emailDeliveryRequestSetType = (Class<Set<EmailDeliveryRequest>>) (Object) Set.class;
+ private ArgumentCaptor<Set<EmailDeliveryRequest>> emailDeliveryRequestSetCaptor = ArgumentCaptor.forClass(emailDeliveryRequestSetType);
@Test
public void getMetadata_returns_same_instance_as_static_method() {
@@ -89,7 +107,7 @@ public class ChangesOnMyIssueNotificationHandlerTest {
@Test
public void getNotificationClass_is_IssueChangeNotification() {
- assertThat(underTest.getNotificationClass()).isEqualTo(IssueChangeNotification.class);
+ assertThat(underTest.getNotificationClass()).isEqualTo(IssuesChangesNotification.class);
}
@Test
@@ -104,8 +122,8 @@ public class ChangesOnMyIssueNotificationHandlerTest {
@Test
public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
when(emailNotificationChannel.isActivated()).thenReturn(false);
- Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> mock(IssueChangeNotification.class))
+ Set<IssuesChangesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
+ .mapToObj(i -> mock(IssuesChangesNotification.class))
.collect(toSet());
int deliver = underTest.deliver(notifications);
@@ -118,29 +136,47 @@ public class ChangesOnMyIssueNotificationHandlerTest {
}
@Test
- public void deliver_has_no_effect_if_no_notification_has_projectKey() {
+ public void deliver_has_no_effect_if_no_notification_has_assignee() {
when(emailNotificationChannel.isActivated()).thenReturn(true);
- Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> newNotification(null, null, null))
+ Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i)
+ .setNewStatus("foo")
+ .setAssignee(null)
+ .setRule(newRule())
+ .setProject(newProject(i + ""))
+ .build())
.collect(toSet());
+ IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder(issues, new UserChange(new Random().nextLong(), new User("user_uuid", "user_login", null)));
- int deliver = underTest.deliver(notifications);
+ int deliver = underTest.deliver(ImmutableSet.of(serializer.serialize(builder)));
assertThat(deliver).isZero();
verifyZeroInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
- notifications.forEach(notification -> {
- verify(notification).getProjectKey();
- verifyNoMoreInteractions(notification);
- });
}
@Test
- public void deliver_has_no_effect_if_no_notification_has_assignee() {
+ public void deliver_has_no_effect_if_all_issues_are_assigned_to_the_changeAuthor() {
when(emailNotificationChannel.isActivated()).thenReturn(true);
- Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> newNotification(randomAlphabetic(5 + i), null, NO_CHANGE_AUTHOR))
+ Set<UserChange> userChanges = IntStream.range(0, 1 + new Random().nextInt(3))
+ .mapToObj(i -> new UserChange(new Random().nextLong(), new User("user_uuid_" + i, "user_login_" + i, null)))
+ .collect(toSet());
+ Set<IssuesChangesNotificationBuilder> notificationBuilders = userChanges.stream()
+ .map(userChange -> {
+ Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i + userChange.getUser().getUuid())
+ .setNewStatus("foo")
+ .setAssignee(userChange.getUser())
+ .setRule(newRule())
+ .setProject(newProject(i + ""))
+ .build())
+ .collect(toSet());
+ return new IssuesChangesNotificationBuilder(issues, userChange);
+ })
+ .collect(toSet());
+ Set<IssuesChangesNotification> notifications = notificationBuilders.stream()
+ .map(t -> serializer.serialize(t))
.collect(toSet());
int deliver = underTest.deliver(notifications);
@@ -149,150 +185,269 @@ public class ChangesOnMyIssueNotificationHandlerTest {
verifyZeroInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
- notifications.forEach(notification -> {
- verify(notification).getProjectKey();
- verify(notification).getAssignee();
- verifyNoMoreInteractions(notification);
- });
}
@Test
- public void deliver_has_no_effect_if_no_notification_has_change_author_different_from_assignee() {
+ public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_ChangesOnMyIssues_notifications() {
when(emailNotificationChannel.isActivated()).thenReturn(true);
- Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> {
- String assignee = randomAlphabetic(4 + i);
- return newNotification(randomAlphabetic(5 + i), assignee, assignee);
- })
+ Project project = newProject();
+ Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i)
+ .setNewStatus("foo")
+ .setAssignee(newUser("assignee_" + i))
+ .setRule(newRule())
+ .setProject(project)
+ .build())
.collect(toSet());
+ IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder(issues, new UserChange(new Random().nextLong(), new User("user_uuid", "user_login", null)));
- int deliver = underTest.deliver(notifications);
+ int deliver = underTest.deliver(ImmutableSet.of(serializer.serialize(builder)));
assertThat(deliver).isZero();
- verifyZeroInteractions(notificationManager);
+ Set<String> assigneeLogins = issues.stream().map(i -> i.getAssignee().get().getLogin()).collect(toSet());
+ verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project.getKey(), assigneeLogins, ALL_MUST_HAVE_ROLE_USER);
+ verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
- notifications.forEach(notification -> {
- verify(notification).getProjectKey();
- verify(notification, times(2)).getAssignee();
- verify(notification).getChangeAuthor();
- verifyNoMoreInteractions(notification);
- });
}
@Test
- @UseDataProvider("noOrDifferentChangeAuthor")
- public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_ChangesOnMyIssues_notifications(@Nullable String noOrDifferentChangeAuthor) {
- String projectKey1 = randomAlphabetic(10);
- String assignee1 = randomAlphabetic(11);
- String projectKey2 = randomAlphabetic(12);
- String assignee2 = randomAlphabetic(13);
- Set<IssueChangeNotification> notifications1 = randomSetOfNotifications(projectKey1, assignee1, noOrDifferentChangeAuthor);
- Set<IssueChangeNotification> notifications2 = randomSetOfNotifications(projectKey2, assignee2, noOrDifferentChangeAuthor);
+ public void deliver_checks_by_projectKeys_if_notifications_have_subscribed_assignee_to_ChangesOnMyIssues_notifications() {
when(emailNotificationChannel.isActivated()).thenReturn(true);
+ Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i)
+ .setNewStatus("foo")
+ .setAssignee(newUser("" + i))
+ .setRule(newRule())
+ .setProject(newProject(i + ""))
+ .build())
+ .collect(toSet());
+ IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder(issues, new UserChange(new Random().nextLong(), new User("user_uuid", "user_login", null)));
- int deliver = underTest.deliver(Stream.concat(notifications1.stream(), notifications2.stream()).collect(toSet()));
+ int deliver = underTest.deliver(ImmutableSet.of(serializer.serialize(builder)));
assertThat(deliver).isZero();
- verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey1, singleton(assignee1), ALL_MUST_HAVE_ROLE_USER);
- verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey2, singleton(assignee2), ALL_MUST_HAVE_ROLE_USER);
+ issues.stream()
+ .collect(MoreCollectors.index(ChangedIssue::getProject))
+ .asMap()
+ .forEach((key, value) -> {
+ String projectKey = key.getKey();
+ Set<String> assigneeLogins = value.stream().map(i -> i.getAssignee().get().getLogin()).collect(toSet());
+ verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, assigneeLogins, ALL_MUST_HAVE_ROLE_USER);
+ });
verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
}
@Test
- @UseDataProvider("noOrDifferentChangeAuthor")
- public void deliver_ignores_notifications_which_assignee_has_not_subscribed_to_ChangesOnMyIssues_notifications(@Nullable String noOrDifferentChangeAuthor) {
- String projectKey = randomAlphabetic(5);
- String assignee1 = randomAlphabetic(6);
- String assignee2 = randomAlphabetic(7);
- // assignee1 is not authorized
- Set<IssueChangeNotification> assignee1Notifications = randomSetOfNotifications(projectKey, assignee1, noOrDifferentChangeAuthor);
- // assignee2 is authorized
- Set<IssueChangeNotification> assignee2Notifications = randomSetOfNotifications(projectKey, assignee2, noOrDifferentChangeAuthor);
+ @UseDataProvider("userOrAnalysisChange")
+ public void deliver_creates_a_notification_per_assignee_with_only_his_issues_on_the_single_project(Change userOrAnalysisChange) {
when(emailNotificationChannel.isActivated()).thenReturn(true);
- when(notificationManager.findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, ImmutableSet.of(assignee1, assignee2), ALL_MUST_HAVE_ROLE_USER))
- .thenReturn(ImmutableSet.of(emailRecipientOf(assignee2)));
- Set<EmailDeliveryRequest> expectedRequests = assignee2Notifications.stream()
- .map(t -> new EmailDeliveryRequest(emailOf(t.getAssignee()), t))
+ Project project = newProject();
+ User assignee1 = newUser("assignee_1");
+ User assignee2 = newUser("assignee_2");
+ Set<ChangedIssue> assignee1Issues = IntStream.range(0, 10)
+ .mapToObj(i -> newChangedIssue("1_issue_key_" + i, assignee1, project))
.collect(toSet());
- int deliveredCount = new Random().nextInt(expectedRequests.size());
- when(emailNotificationChannel.deliverAll(expectedRequests)).thenReturn(deliveredCount);
+ Set<ChangedIssue> assignee2Issues = IntStream.range(0, 10)
+ .mapToObj(i -> newChangedIssue("2_issue_key_" + i, assignee2, project))
+ .collect(toSet());
+ Set<IssuesChangesNotification> notifications = Stream.of(
+ // notification with only assignee1 5 notifications
+ new IssuesChangesNotificationBuilder(assignee1Issues.stream().limit(5).collect(toSet()), userOrAnalysisChange),
+ // notification with only assignee2 6 notifications
+ new IssuesChangesNotificationBuilder(assignee2Issues.stream().limit(6).collect(toSet()), userOrAnalysisChange),
+ // notification with 4 assignee1 and 3 assignee2 notifications
+ new IssuesChangesNotificationBuilder(
+ Stream.concat(assignee1Issues.stream().skip(6), assignee2Issues.stream().skip(7)).collect(toSet()),
+ userOrAnalysisChange))
+ .map(t -> serializer.serialize(t))
+ .collect(toSet());
+ when(notificationManager.findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project.getKey(), ImmutableSet.of(assignee1.getLogin(), assignee2.getLogin()),
+ ALL_MUST_HAVE_ROLE_USER))
+ .thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin()), emailRecipientOf(assignee2.getLogin())));
+ int deliveredCount = new Random().nextInt(100);
+ when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount);
- int deliver = underTest.deliver(Stream.concat(assignee1Notifications.stream(), assignee2Notifications.stream()).collect(toSet()));
+ int deliver = underTest.deliver(notifications);
assertThat(deliver).isEqualTo(deliveredCount);
- verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, ImmutableSet.of(assignee1, assignee2), ALL_MUST_HAVE_ROLE_USER);
+ verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY,
+ project.getKey(), ImmutableSet.of(assignee1.getLogin(), assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER);
verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
- verify(emailNotificationChannel).deliverAll(expectedRequests);
+ verify(emailNotificationChannel).deliverAll(emailDeliveryRequestSetCaptor.capture());
verifyNoMoreInteractions(emailNotificationChannel);
+
+ Set<EmailDeliveryRequest> emailDeliveryRequests = emailDeliveryRequestSetCaptor.getValue();
+ assertThat(emailDeliveryRequests).hasSize(4);
+ ListMultimap<String, EmailDeliveryRequest> emailDeliveryRequestByEmail = emailDeliveryRequests.stream()
+ .collect(index(EmailDeliveryRequest::getRecipientEmail));
+ List<EmailDeliveryRequest> assignee1Requests = emailDeliveryRequestByEmail.get(emailOf(assignee1.getLogin()));
+ assertThat(assignee1Requests)
+ .hasSize(2)
+ .extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification())
+ .extracting(ChangesOnMyIssuesNotification::getChange)
+ .containsOnly(userOrAnalysisChange);
+ assertThat(assignee1Requests)
+ .extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification())
+ .extracting(ChangesOnMyIssuesNotification::getChangedIssues)
+ .containsOnly(
+ assignee1Issues.stream().limit(5).collect(unorderedIndex(t -> project, t -> t)),
+ assignee1Issues.stream().skip(6).collect(unorderedIndex(t -> project, t -> t)));
+
+ List<EmailDeliveryRequest> assignee2Requests = emailDeliveryRequestByEmail.get(emailOf(assignee2.getLogin()));
+ assertThat(assignee2Requests)
+ .hasSize(2)
+ .extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification())
+ .extracting(ChangesOnMyIssuesNotification::getChange)
+ .containsOnly(userOrAnalysisChange);
+ assertThat(assignee2Requests)
+ .extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification())
+ .extracting(ChangesOnMyIssuesNotification::getChangedIssues)
+ .containsOnly(
+ assignee2Issues.stream().limit(6).collect(unorderedIndex(t -> project, t -> t)),
+ assignee2Issues.stream().skip(7).collect(unorderedIndex(t -> project, t -> t)));
}
@Test
- public void deliver_ignores_notifications_which_assignee_is_the_changeAuthor() {
- String projectKey = randomAlphabetic(5);
- String assignee1 = randomAlphabetic(6);
- String assignee2 = randomAlphabetic(7);
- String assignee3 = randomAlphabetic(8);
- // assignee1 is the changeAuthor of every notification he's the assignee of
- Set<IssueChangeNotification> assignee1ChangeAuthor = randomSetOfNotifications(projectKey, assignee1, assignee1);
- // assignee2 is the changeAuthor of some notification he's the assignee of
- Set<IssueChangeNotification> assignee2ChangeAuthor = randomSetOfNotifications(projectKey, assignee2, assignee2);
- Set<IssueChangeNotification> assignee2NotChangeAuthor = randomSetOfNotifications(projectKey, assignee2, randomAlphabetic(10));
- Set<IssueChangeNotification> assignee2NoChangeAuthor = randomSetOfNotifications(projectKey, assignee2, NO_CHANGE_AUTHOR);
- // assignee3 is never the changeAuthor of the notification he's the assignee of
- Set<IssueChangeNotification> assignee3NotChangeAuthor = randomSetOfNotifications(projectKey, assignee3, randomAlphabetic(11));
- Set<IssueChangeNotification> assignee3NoChangeAuthor = randomSetOfNotifications(projectKey, assignee3, NO_CHANGE_AUTHOR);
+ @UseDataProvider("userOrAnalysisChange")
+ public void deliver_ignores_issues_which_assignee_is_the_changeAuthor(Change userOrAnalysisChange) {
when(emailNotificationChannel.isActivated()).thenReturn(true);
- // assignees which are not changeAuthor have subscribed
- Set<String> assigneesChangeAuthor = ImmutableSet.of(assignee2, assignee3);
- when(notificationManager.findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, assigneesChangeAuthor, ALL_MUST_HAVE_ROLE_USER))
- .thenReturn(ImmutableSet.of(emailRecipientOf(assignee2), emailRecipientOf(assignee3)));
- Set<EmailDeliveryRequest> expectedRequests = Stream.of(
- assignee2NotChangeAuthor.stream(), assignee2NoChangeAuthor.stream(),
- assignee3NotChangeAuthor.stream(), assignee3NoChangeAuthor.stream())
- .flatMap(t -> t)
- .map(t -> new EmailDeliveryRequest(emailOf(t.getAssignee()), t))
+ Project project1 = newProject();
+ Project project2 = newProject();
+ User assignee1 = newUser("assignee_1");
+ User assignee2 = newUser("assignee_2");
+ Set<ChangedIssue> assignee1Issues = IntStream.range(0, 10)
+ .mapToObj(i -> newChangedIssue("1_issue_key_" + i, assignee1, project1))
+ .collect(toSet());
+ Set<ChangedIssue> assignee2Issues = IntStream.range(0, 10)
+ .mapToObj(i -> newChangedIssue("2_issue_key_" + i, assignee2, project2))
.collect(toSet());
- int deliveredCount = new Random().nextInt(expectedRequests.size());
- when(emailNotificationChannel.deliverAll(expectedRequests)).thenReturn(deliveredCount);
- Set<IssueChangeNotification> notifications = Stream.of(
- assignee1ChangeAuthor.stream(),
- assignee2ChangeAuthor.stream(), assignee2NotChangeAuthor.stream(), assignee2NoChangeAuthor.stream(),
- assignee3NotChangeAuthor.stream(), assignee3NoChangeAuthor.stream()).flatMap(t -> t)
+ UserChange assignee2Change1 = new UserChange(new Random().nextLong(), assignee2);
+ Set<IssuesChangesNotification> notifications = Stream.of(
+ // notification from assignee1 with issues from assignee1 only
+ new IssuesChangesNotificationBuilder(
+ assignee1Issues.stream().limit(4).collect(toSet()),
+ new UserChange(new Random().nextLong(), assignee1)),
+ // notification from assignee2 with issues from assignee1 and assignee2
+ new IssuesChangesNotificationBuilder(
+ Stream.concat(
+ assignee1Issues.stream().skip(4).limit(2),
+ assignee2Issues.stream().limit(4))
+ .collect(toSet()),
+ assignee2Change1),
+ // notification from assignee2 with issues from assignee2 only
+ new IssuesChangesNotificationBuilder(
+ assignee2Issues.stream().skip(4).limit(3).collect(toSet()),
+ new UserChange(new Random().nextLong(), assignee2)),
+ // notification from other change with issues from assignee1 and assignee2)
+ new IssuesChangesNotificationBuilder(
+ Stream.concat(
+ assignee1Issues.stream().skip(6),
+ assignee2Issues.stream().skip(7))
+ .collect(toSet()),
+ userOrAnalysisChange))
+ .map(t -> serializer.serialize(t))
.collect(toSet());
+ when(notificationManager.findSubscribedEmailRecipients(
+ CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project1.getKey(), ImmutableSet.of(assignee1.getLogin()), ALL_MUST_HAVE_ROLE_USER))
+ .thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin())));
+ when(notificationManager.findSubscribedEmailRecipients(
+ CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project2.getKey(), ImmutableSet.of(assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER))
+ .thenReturn(ImmutableSet.of(emailRecipientOf(assignee2.getLogin())));
+ int deliveredCount = new Random().nextInt(100);
+ when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount);
+
int deliver = underTest.deliver(notifications);
assertThat(deliver).isEqualTo(deliveredCount);
- verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, assigneesChangeAuthor, ALL_MUST_HAVE_ROLE_USER);
+ verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY,
+ project1.getKey(), ImmutableSet.of(assignee1.getLogin()), ALL_MUST_HAVE_ROLE_USER);
+ verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY,
+ project2.getKey(), ImmutableSet.of(assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER);
verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
- verify(emailNotificationChannel).deliverAll(expectedRequests);
+ verify(emailNotificationChannel).deliverAll(emailDeliveryRequestSetCaptor.capture());
verifyNoMoreInteractions(emailNotificationChannel);
+
+ Set<EmailDeliveryRequest> emailDeliveryRequests = emailDeliveryRequestSetCaptor.getValue();
+ assertThat(emailDeliveryRequests).hasSize(3);
+ ListMultimap<String, EmailDeliveryRequest> emailDeliveryRequestByEmail = emailDeliveryRequests.stream()
+ .collect(index(EmailDeliveryRequest::getRecipientEmail));
+ List<EmailDeliveryRequest> assignee1Requests = emailDeliveryRequestByEmail.get(emailOf(assignee1.getLogin()));
+ assertThat(assignee1Requests)
+ .hasSize(2)
+ .extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification())
+ .extracting(ChangesOnMyIssuesNotification::getChange)
+ .containsOnly(userOrAnalysisChange, assignee2Change1);
+ assertThat(assignee1Requests)
+ .extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification())
+ .extracting(ChangesOnMyIssuesNotification::getChangedIssues)
+ .containsOnly(
+ assignee1Issues.stream().skip(4).limit(2).collect(unorderedIndex(t -> project1, t -> t)),
+ assignee1Issues.stream().skip(6).collect(unorderedIndex(t -> project1, t -> t)));
+
+ List<EmailDeliveryRequest> assignee2Requests = emailDeliveryRequestByEmail.get(emailOf(assignee2.getLogin()));
+ assertThat(assignee2Requests)
+ .hasSize(1)
+ .extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification())
+ .extracting(ChangesOnMyIssuesNotification::getChange)
+ .containsOnly(userOrAnalysisChange);
+ assertThat(assignee2Requests)
+ .extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification())
+ .extracting(ChangesOnMyIssuesNotification::getChangedIssues)
+ .containsOnly(assignee2Issues.stream().skip(7).collect(unorderedIndex(t -> project2, t -> t)));
}
@DataProvider
- public static Object[][] noOrDifferentChangeAuthor() {
+ public static Object[][] userOrAnalysisChange() {
+ User changeAuthor = new User(randomAlphabetic(12), randomAlphabetic(10), randomAlphabetic(11));
return new Object[][] {
- {NO_CHANGE_AUTHOR},
- {randomAlphabetic(15)}
+ {new AnalysisChange(new Random().nextLong())},
+ {new UserChange(new Random().nextLong(), changeAuthor)},
};
}
- private static Set<IssueChangeNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) {
+ private static Project newProject() {
+ String base = randomAlphabetic(6);
+ return newProject(base);
+ }
+
+ private static Project newProject(String base) {
+ return new Project.Builder("prj_uuid_" + base)
+ .setKey("prj_key_" + base)
+ .setProjectName("prj_name_" + base)
+ .build();
+ }
+
+ private static User newUser(String name) {
+ return new User(name + "_uuid", name + "login", name);
+ }
+
+ private static ChangedIssue newChangedIssue(String key, User assignee1, Project project) {
+ return new ChangedIssue.Builder(key)
+ .setNewStatus("foo")
+ .setAssignee(assignee1)
+ .setRule(newRule())
+ .setProject(project)
+ .build();
+ }
+
+ private static Rule newRule() {
+ return new Rule(RuleKey.of(randomAlphabetic(3), randomAlphabetic(4)), randomAlphabetic(5));
+ }
+
+ private static Set<IssuesChangesNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) {
return IntStream.range(0, 1 + new Random().nextInt(5))
.mapToObj(i -> newNotification(projectKey, assignee, changeAuthor))
.collect(Collectors.toSet());
}
- private static IssueChangeNotification newNotification(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) {
- IssueChangeNotification notification = mock(IssueChangeNotification.class);
- when(notification.getProjectKey()).thenReturn(projectKey);
- when(notification.getAssignee()).thenReturn(assignee);
- when(notification.getChangeAuthor()).thenReturn(changeAuthor);
+ private static IssuesChangesNotification newNotification(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) {
+ IssuesChangesNotification notification = mock(IssuesChangesNotification.class);
return notification;
}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplateTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplateTest.java
new file mode 100644
index 00000000000..0a2e097a379
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplateTest.java
@@ -0,0 +1,721 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.ImmutableSet;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.notifications.Notification;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
+import org.sonar.test.html.HtmlFragmentAssert;
+import org.sonar.test.html.HtmlListAssert;
+import org.sonar.test.html.HtmlParagraphAssert;
+
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.issue.Issue.STATUS_CLOSED;
+import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
+import static org.sonar.api.issue.Issue.STATUS_OPEN;
+import static org.sonar.api.issue.Issue.STATUS_REOPENED;
+import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newBranch;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newChangedIssue;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRule;
+
+@RunWith(DataProviderRunner.class)
+public class ChangesOnMyIssuesEmailTemplateTest {
+ private static final String[] ISSUE_STATUSES = {STATUS_OPEN, STATUS_RESOLVED, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_CLOSED};
+ @org.junit.Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private I18n i18n = mock(I18n.class);
+ private EmailSettings emailSettings = mock(EmailSettings.class);
+ private ChangesOnMyIssuesEmailTemplate underTest = new ChangesOnMyIssuesEmailTemplate(i18n, emailSettings);
+
+ @Test
+ public void format_returns_null_on_Notification() {
+ EmailMessage emailMessage = underTest.format(mock(Notification.class));
+
+ assertThat(emailMessage).isNull();
+ }
+
+ @Test
+ public void formats_fails_with_ISE_if_change_from_Analysis_and_no_issue() {
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("changedIssues can't be empty");
+
+ underTest.format(new ChangesOnMyIssuesNotification(analysisChange, Collections.emptySet()));
+ }
+
+ @Test
+ public void format_sets_message_id_with_project_key_of_first_issue_in_set_when_change_from_Analysis() {
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+ .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+ .collect(toSet());
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
+
+ assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues/" + changedIssues.iterator().next().getProject().getKey());
+ }
+
+ @Test
+ public void format_sets_subject_with_project_name_of_first_issue_in_set_when_change_from_Analysis() {
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+ .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+ .collect(toSet());
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
+
+ Project project = changedIssues.iterator().next().getProject();
+ assertThat(emailMessage.getSubject()).isEqualTo("Analysis has changed some of your issues in " + project.getProjectName());
+ }
+
+ @Test
+ public void format_sets_subject_with_project_name_and_branch_name_of_first_issue_in_set_when_change_from_Analysis() {
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+ .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newBranch("prj_" + i, "br_" + i), newRule("rule_" + i)))
+ .collect(toSet());
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
+
+ Project project = changedIssues.iterator().next().getProject();
+ assertThat(emailMessage.getSubject()).isEqualTo("Analysis has changed some of your issues in " + project.getProjectName() + ", " + project.getBranchName().get());
+ }
+
+ @Test
+ public void format_set_html_message_with_header_dealing_with_plural_when_change_from_Analysis() {
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+ .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+ .collect(toSet());
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+
+ EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues.stream().limit(1).collect(toSet())));
+ EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
+
+ HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
+ .hasParagraph("Hi,")
+ .hasParagraph("An analysis has updated an issue assigned to you:");
+ HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
+ .hasParagraph("Hi,")
+ .hasParagraph("An analysis has updated issues assigned to you:");
+ }
+
+ @Test
+ public void format_sets_static_message_id_when_change_from_User() {
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+ .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+ .collect(toSet());
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
+
+ assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues");
+ }
+
+ @Test
+ public void format_sets_static_subject_when_change_from_User() {
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+ .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+ .collect(toSet());
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
+
+ assertThat(emailMessage.getSubject()).isEqualTo("A manual update has changed some of your issues");
+ }
+
+ @Test
+ public void format_set_html_message_with_header_dealing_with_plural_when_change_from_User() {
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+ .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+ .collect(toSet());
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+
+ EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
+ userChange, changedIssues.stream().limit(1).collect(toSet())));
+ EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
+
+ HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
+ .hasParagraph("Hi,")
+ .withoutLink()
+ .hasParagraph("A manual change has updated an issue assigned to you:")
+ .withoutLink();
+ HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
+ .hasParagraph("Hi,")
+ .withoutLink()
+ .hasParagraph("A manual change has updated issues assigned to you:")
+ .withoutLink();
+ }
+
+ @Test
+ @UseDataProvider("issueStatuses")
+ public void format_set_html_message_with_footer_when_change_from_user(String issueStatus) {
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+ format_set_html_message_with_footer(userChange, issueStatus, c -> c
+ // skip content
+ .hasParagraph() // open/closed issue
+ .hasList() // rule list
+ );
+ }
+
+ @Test
+ @UseDataProvider("issueStatuses")
+ public void format_set_html_message_with_footer_when_change_from_analysis(String issueStatus) {
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+ format_set_html_message_with_footer(analysisChange, issueStatus, c -> c
+ // skip content
+ .hasParagraph() // status
+ .hasList() // rule list
+ );
+ }
+
+ @DataProvider
+ public static Object[][] issueStatuses() {
+ return Arrays.stream(ISSUE_STATUSES)
+ .map(t -> new Object[] {t})
+ .toArray(Object[][]::new);
+ }
+
+ private void format_set_html_message_with_footer(Change change, String issueStatus, Function<HtmlParagraphAssert, HtmlListAssert> skipContent) {
+ String wordingNotification = randomAlphabetic(20);
+ String host = randomAlphabetic(15);
+ String instance = randomAlphabetic(17);
+ when(i18n.message(Locale.ENGLISH, "notification.dispatcher.ChangesOnMyIssue", "notification.dispatcher.ChangesOnMyIssue"))
+ .thenReturn(wordingNotification);
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+ when(emailSettings.getInstanceName()).thenReturn(instance);
+ Project project = newProject("foo");
+ Rule rule = newRule("bar");
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+ .mapToObj(i -> newChangedIssue(i + "", issueStatus, project, rule))
+ .collect(toSet());
+
+ EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
+ change, changedIssues.stream().limit(1).collect(toSet())));
+ EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(change, changedIssues));
+
+ Stream.of(singleIssueMessage, multiIssueMessage)
+ .forEach(issueMessage -> {
+ HtmlParagraphAssert htmlAssert = HtmlFragmentAssert.assertThat(issueMessage.getMessage())
+ .hasParagraph().hasParagraph(); // skip header
+ // skip content
+ HtmlListAssert htmlListAssert = skipContent.apply(htmlAssert);
+
+ String footerText = "You received this email because you are subscribed to \"" + wordingNotification + "\" notifications from " + instance + "."
+ + " Click here to edit your email preferences.";
+ htmlListAssert.hasEmptyParagraph()
+ .hasParagraph(footerText)
+ .withSmallOn(footerText)
+ .withLink("here", host + "/account/notifications")
+ .noMoreBlock();
+ });
+ }
+
+ @Test
+ public void format_set_html_message_with_issues_grouped_by_status_closed_or_any_other_when_change_from_analysis() {
+ Project project = newProject("foo");
+ Rule rule = newRule("bar");
+ Set<ChangedIssue> changedIssues = Arrays.stream(ISSUE_STATUSES)
+ .map(status -> newChangedIssue(status + "", status, project, rule))
+ .collect(toSet());
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
+
+ HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph("Closed issue:")
+ .withoutLink()
+ .hasList("Rule " + rule.getName() + " - See the single issue")
+ .withLinkOn("See the single issue")
+ .hasParagraph("Open issues:")
+ .withoutLink()
+ .hasList("Rule " + rule.getName() + " - See all " + (ISSUE_STATUSES.length - 1) + " issues")
+ .withLinkOn("See all " + (ISSUE_STATUSES.length - 1) + " issues");
+ verifyEnd(htmlListAssert);
+ }
+
+ @Test
+ public void format_set_html_message_with_status_title_handles_plural_when_change_from_analysis() {
+ Project project = newProject("foo");
+ Rule rule = newRule("bar");
+ Set<ChangedIssue> closedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(status -> newChangedIssue(status + "", STATUS_CLOSED, project, rule))
+ .collect(toSet());
+ Set<ChangedIssue> openIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(status -> newChangedIssue(status + "", STATUS_OPEN, project, rule))
+ .collect(toSet());
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+
+ EmailMessage closedIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, closedIssues));
+ EmailMessage openIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, openIssues));
+
+ HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(closedIssuesMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph("Closed issues:")
+ .hasList();
+ verifyEnd(htmlListAssert);
+ htmlListAssert = HtmlFragmentAssert.assertThat(openIssuesMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph("Open issues:")
+ .hasList();
+ verifyEnd(htmlListAssert);
+ }
+
+ @Test
+ public void formats_returns_html_message_for_single_issue_on_master_when_analysis_change() {
+ Project project = newProject("1");
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName);
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph()// skip title based on status
+ .hasList("Rule " + ruleName + " - See the single issue")
+ .withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_for_single_issue_on_master_when_user_change() {
+ Project project = newProject("1");
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName);
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName())
+ .hasList("Rule " + ruleName + " - See the single issue")
+ .withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_for_single_issue_on_branch_when_analysis_change() {
+ String branchName = randomAlphabetic(6);
+ Project project = newBranch("1", branchName);
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ String key = "key";
+ ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName);
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph()// skip title based on status
+ .hasList("Rule " + ruleName + " - See the single issue")
+ .withLink("See the single issue",
+ host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_for_single_issue_on_branch_when_user_change() {
+ String branchName = randomAlphabetic(6);
+ Project project = newBranch("1", branchName);
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ String key = "key";
+ ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName);
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName() + ", " + branchName)
+ .hasList("Rule " + ruleName + " - See the single issue")
+ .withLink("See the single issue",
+ host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master_when_analysis_change() {
+ Project project = newProject("1");
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ Rule rule = newRule(ruleName);
+ String issueStatus = randomValidStatus();
+ List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(i -> newChangedIssue("issue_" + i, issueStatus, project, rule))
+ .collect(toList());
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
+
+ String expectedHref = host + "/project/issues?id=" + project.getKey()
+ + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
+ String expectedLinkText = "See all " + changedIssues.size() + " issues";
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph() // skip title based on status
+ .hasList("Rule " + ruleName + " - " + expectedLinkText)
+ .withLink(expectedLinkText, expectedHref)
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master_when_user_change() {
+ Project project = newProject("1");
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ Rule rule = newRule(ruleName);
+ List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
+ .collect(toList());
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
+
+ String expectedHref = host + "/project/issues?id=" + project.getKey()
+ + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
+ String expectedLinkText = "See all " + changedIssues.size() + " issues";
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName())
+ .hasList("Rule " + ruleName + " - " + expectedLinkText)
+ .withLink(expectedLinkText, expectedHref)
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch_when_analysis_change() {
+ String branchName = randomAlphabetic(19);
+ Project project = newBranch("1", branchName);
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ Rule rule = newRule(ruleName);
+ String status = randomValidStatus();
+ List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(i -> newChangedIssue("issue_" + i, status, project, rule))
+ .collect(toList());
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
+
+ String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
+ + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
+ String expectedLinkText = "See all " + changedIssues.size() + " issues";
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph()// skip title based on status
+ .hasList("Rule " + ruleName + " - " + expectedLinkText)
+ .withLink(expectedLinkText, expectedHref)
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch_when_user_change() {
+ String branchName = randomAlphabetic(19);
+ Project project = newBranch("1", branchName);
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ Rule rule = newRule(ruleName);
+ List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
+ .collect(toList());
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
+
+ String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
+ + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
+ String expectedLinkText = "See all " + changedIssues.size() + " issues";
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName() + ", " + branchName)
+ .hasList("Rule " + ruleName + " - " + expectedLinkText)
+ .withLink(expectedLinkText, expectedHref)
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_with_projects_ordered_by_name_when_user_change() {
+ Project project1 = newProject("1");
+ Project project1Branch1 = newBranch("1", "a");
+ Project project1Branch2 = newBranch("1", "b");
+ Project project2 = newProject("B");
+ Project project2Branch1 = newBranch("B", "a");
+ Project project3 = newProject("C");
+ String host = randomAlphabetic(15);
+ List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3)
+ .map(project -> newChangedIssue("issue_" + project.getUuid(), randomValidStatus(), project, newRule(randomAlphabetic(2))))
+ .collect(toList());
+ Collections.shuffle(changedIssues);
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project1.getProjectName())
+ .hasList()
+ .hasParagraph(project1Branch1.getProjectName() + ", " + project1Branch1.getBranchName().get())
+ .hasList()
+ .hasParagraph(project1Branch2.getProjectName() + ", " + project1Branch2.getBranchName().get())
+ .hasList()
+ .hasParagraph(project2.getProjectName())
+ .hasList()
+ .hasParagraph(project2Branch1.getProjectName() + ", " + project2Branch1.getBranchName().get())
+ .hasList()
+ .hasParagraph(project3.getProjectName())
+ .hasList()
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change() {
+ Project project = newProject("1");
+ Rule rule1 = newRule("1");
+ Rule rule2 = newRule("a");
+ Rule rule3 = newRule("b");
+ Rule rule4 = newRule("X");
+ String host = randomAlphabetic(15);
+ String issueStatus = randomValidStatus();
+ List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
+ .map(rule -> newChangedIssue("issue_" + rule.getName(), issueStatus, project, rule))
+ .collect(toList());
+ Collections.shuffle(changedIssues);
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph()// skip title based on status
+ .hasList(
+ "Rule " + rule1.getName() + " - See the single issue",
+ "Rule " + rule2.getName() + " - See the single issue",
+ "Rule " + rule3.getName() + " - See the single issue",
+ "Rule " + rule4.getName() + " - See the single issue")
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change_when_user_analysis() {
+ Project project = newProject("1");
+ Rule rule1 = newRule("1");
+ Rule rule2 = newRule("a");
+ Rule rule3 = newRule("b");
+ Rule rule4 = newRule("X");
+ String host = randomAlphabetic(15);
+ List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
+ .map(rule -> newChangedIssue("issue_" + rule.getName(), randomValidStatus(), project, rule))
+ .collect(toList());
+ Collections.shuffle(changedIssues);
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName())
+ .hasList(
+ "Rule " + rule1.getName() + " - See the single issue",
+ "Rule " + rule2.getName() + " - See the single issue",
+ "Rule " + rule3.getName() + " - See the single issue",
+ "Rule " + rule4.getName() + " - See the single issue")
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_analysis_change() {
+ Project project1 = newProject("1");
+ Rule rule1 = newRule("1");
+ Rule rule2 = newRule("a");
+ String host = randomAlphabetic(15);
+ String issueStatusClosed = STATUS_CLOSED;
+ String otherIssueStatus = STATUS_RESOLVED;
+ List<ChangedIssue> changedIssues = Stream.of(
+ IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, issueStatusClosed, project1, rule1)),
+ IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, issueStatusClosed, project1, rule2)),
+ IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, otherIssueStatus, project1, rule2)),
+ IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, otherIssueStatus, project1, rule1)))
+ .flatMap(t -> t)
+ .collect(toList());
+ Collections.shuffle(changedIssues);
+ AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph("Closed issues:") // skip title based on status
+ .hasList(
+ "Rule " + rule1.getName() + " - See all 39 issues",
+ "Rule " + rule2.getName() + " - See all 40 issues")
+ .withLink("See all 39 issues",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
+ .withLink("See all 40 issues",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
+ .hasParagraph("Open issues:")
+ .hasList(
+ "Rule " + rule2.getName() + " - See issues 1-40 41-80 81",
+ "Rule " + rule1.getName() + " - See all 6 issues")
+ .withLink("1-40",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
+ .withLink("41-80",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
+ .withLink("81",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
+ .withLink("See all 6 issues",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_user_change() {
+ Project project1 = newProject("1");
+ Project project2 = newProject("V");
+ Project project2Branch = newBranch("V", "AB");
+ Rule rule1 = newRule("1");
+ Rule rule2 = newRule("a");
+ String status = randomValidStatus();
+ String host = randomAlphabetic(15);
+ List<ChangedIssue> changedIssues = Stream.of(
+ IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, status, project1, rule1)),
+ IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, status, project1, rule2)),
+ IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, status, project2, rule2)),
+ IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, status, project2Branch, rule1)))
+ .flatMap(t -> t)
+ .collect(toList());
+ Collections.shuffle(changedIssues);
+ UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project1.getProjectName())
+ .hasList()
+ .withItemTexts(
+ "Rule " + rule1.getName() + " - See all 39 issues",
+ "Rule " + rule2.getName() + " - See all 40 issues")
+ .withLink("See all 39 issues",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
+ .withLink("See all 40 issues",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
+ .hasParagraph(project2.getProjectName())
+ .hasList("Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
+ .withLink("1-40",
+ host + "/project/issues?id=" + project2.getKey()
+ + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
+ .withLink("41-80",
+ host + "/project/issues?id=" + project2.getKey()
+ + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
+ .withLink("81",
+ host + "/project/issues?id=" + project2.getKey()
+ + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
+ .hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get())
+ .hasList("Rule " + rule1.getName() + " - See all 6 issues")
+ .withLink("See all 6 issues",
+ host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
+ + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ private static String randomValidStatus() {
+ return ISSUE_STATUSES[new Random().nextInt(ISSUE_STATUSES.length)];
+ }
+
+ private void verifyEnd(HtmlListAssert htmlListAssert) {
+ htmlListAssert
+ .hasEmptyParagraph()
+ .hasParagraph()
+ .noMoreBlock();
+ }
+
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotificationTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotificationTest.java
new file mode 100644
index 00000000000..d3bb3fb19f6
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotificationTest.java
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Random;
+import org.junit.Test;
+import org.sonar.api.notifications.Notification;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class ChangesOnMyIssuesNotificationTest {
+ @Test
+ public void key_is_ChangesOnMyIssues() {
+ ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(
+ new UserChange(new Random().nextLong(), new User(randomAlphabetic(2), randomAlphabetic(3), randomAlphabetic(4))),
+ ImmutableSet.of());
+
+ assertThat(underTest.getType()).isEqualTo("ChangesOnMyIssues");
+ }
+
+ @Test
+ public void equals_is_based_on_change_and_issues() {
+ AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
+ ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"), IssuesChangesNotificationBuilderTesting.newRule("rul"));
+ ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue));
+
+ assertThat(underTest)
+ .isEqualTo(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)))
+ .isNotEqualTo(mock(Notification.class))
+ .isNotEqualTo(null)
+ .isNotEqualTo(new ChangesOnMyIssuesNotification(new AnalysisChange(analysisChange.getDate() + 10), ImmutableSet.of(changedIssue)))
+ .isNotEqualTo(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of()));
+ }
+
+ @Test
+ public void hashcode_is_based_on_change_and_issues() {
+ AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
+ ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"), IssuesChangesNotificationBuilderTesting.newRule("rul"));
+ ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue));
+
+ assertThat(underTest.hashCode())
+ .isEqualTo(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)).hashCode())
+ .isNotEqualTo(mock(Notification.class).hashCode())
+ .isNotEqualTo(null)
+ .isNotEqualTo(new ChangesOnMyIssuesNotification(new AnalysisChange(analysisChange.getDate() + 10), ImmutableSet.of(changedIssue)).hashCode())
+ .isNotEqualTo(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of())).hashCode();
+ }
+
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/DoNotFixNotificationHandlerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/DoNotFixNotificationHandlerTest.java
deleted file mode 100644
index c7990f33d7b..00000000000
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/DoNotFixNotificationHandlerTest.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.server.issue.notification;
-
-import com.google.common.collect.ImmutableSet;
-import com.tngtech.java.junit.dataprovider.DataProvider;
-import com.tngtech.java.junit.dataprovider.DataProviderRunner;
-import com.tngtech.java.junit.dataprovider.UseDataProvider;
-import java.util.Random;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-import javax.annotation.Nullable;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mockito;
-import org.sonar.api.issue.Issue;
-import org.sonar.server.notification.NotificationDispatcherMetadata;
-import org.sonar.server.notification.NotificationManager;
-import org.sonar.server.notification.email.EmailNotificationChannel;
-import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
-
-import static java.util.stream.Collectors.toSet;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
-import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
-import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
-
-@RunWith(DataProviderRunner.class)
-public class DoNotFixNotificationHandlerTest {
- private static final String DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY = "NewFalsePositiveIssue";
- private NotificationManager notificationManager = mock(NotificationManager.class);
- private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
- private DoNotFixNotificationHandler underTest = new DoNotFixNotificationHandler(notificationManager, emailNotificationChannel);
-
- @Test
- public void getMetadata_returns_same_instance_as_static_method() {
- assertThat(underTest.getMetadata().get()).isSameAs(DoNotFixNotificationHandler.newMetadata());
- }
-
- @Test
- public void verify_changeOnMyIssues_notification_dispatcher_key() {
- NotificationDispatcherMetadata metadata = DoNotFixNotificationHandler.newMetadata();
-
- assertThat(metadata.getDispatcherKey()).isEqualTo(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY);
- }
-
- @Test
- public void changeOnMyIssues_notification_is_disabled_at_global_level() {
- NotificationDispatcherMetadata metadata = DoNotFixNotificationHandler.newMetadata();
-
- assertThat(metadata.getProperty(GLOBAL_NOTIFICATION)).isEqualTo("false");
- }
-
- @Test
- public void changeOnMyIssues_notification_is_enable_at_project_level() {
- NotificationDispatcherMetadata metadata = DoNotFixNotificationHandler.newMetadata();
-
- assertThat(metadata.getProperty(PER_PROJECT_NOTIFICATION)).isEqualTo("true");
- }
-
- @Test
- public void getNotificationClass_is_IssueChangeNotification() {
- assertThat(underTest.getNotificationClass()).isEqualTo(IssueChangeNotification.class);
- }
-
- @Test
- public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
- when(emailNotificationChannel.isActivated()).thenReturn(false);
- Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> mock(IssueChangeNotification.class))
- .collect(toSet());
-
- int deliver = underTest.deliver(notifications);
-
- assertThat(deliver).isZero();
- verifyZeroInteractions(notificationManager);
- verify(emailNotificationChannel).isActivated();
- verifyNoMoreInteractions(emailNotificationChannel);
- notifications.forEach(Mockito::verifyZeroInteractions);
- }
-
- @Test
- public void deliver_has_no_effect_if_no_notification_has_projectKey() {
- when(emailNotificationChannel.isActivated()).thenReturn(true);
- Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> newNotification(null, null, null))
- .collect(toSet());
-
- int deliver = underTest.deliver(notifications);
-
- assertThat(deliver).isZero();
- verifyZeroInteractions(notificationManager);
- verify(emailNotificationChannel).isActivated();
- verifyNoMoreInteractions(emailNotificationChannel);
- notifications.forEach(notification -> {
- verify(notification).getProjectKey();
- verifyNoMoreInteractions(notification);
- });
- }
-
- @Test
- public void deliver_has_no_effect_if_no_notification_has_change_author() {
- when(emailNotificationChannel.isActivated()).thenReturn(true);
- Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> newNotification(randomAlphabetic(5 + i), null, null))
- .collect(toSet());
-
- int deliver = underTest.deliver(notifications);
-
- assertThat(deliver).isZero();
- verifyZeroInteractions(notificationManager);
- verify(emailNotificationChannel).isActivated();
- verifyNoMoreInteractions(emailNotificationChannel);
- notifications.forEach(notification -> {
- verify(notification).getProjectKey();
- verify(notification).getChangeAuthor();
- verifyNoMoreInteractions(notification);
- });
- }
-
- @Test
- public void deliver_has_no_effect_if_no_notification_has_new_resolution() {
- when(emailNotificationChannel.isActivated()).thenReturn(true);
- Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> newNotification(randomAlphabetic(5 + i), randomAlphabetic(4 + i), null))
- .collect(toSet());
-
- int deliver = underTest.deliver(notifications);
-
- assertThat(deliver).isZero();
- verifyZeroInteractions(notificationManager);
- verify(emailNotificationChannel).isActivated();
- verifyNoMoreInteractions(emailNotificationChannel);
- notifications.forEach(notification -> {
- verify(notification).getProjectKey();
- verify(notification).getChangeAuthor();
- verify(notification).getNewResolution();
- verifyNoMoreInteractions(notification);
- });
- }
-
- @Test
- @UseDataProvider("notFPorWontFixResolution")
- public void deliver_has_no_effect_if_no_notification_has_FP_or_wont_fix_resolution(String newResolution) {
- when(emailNotificationChannel.isActivated()).thenReturn(true);
- Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> newNotification(randomAlphabetic(5 + i), randomAlphabetic(4 + i), newResolution))
- .collect(toSet());
-
- int deliver = underTest.deliver(notifications);
-
- assertThat(deliver).isZero();
- verifyZeroInteractions(notificationManager);
- verify(emailNotificationChannel).isActivated();
- verifyNoMoreInteractions(emailNotificationChannel);
- notifications.forEach(notification -> {
- verify(notification).getProjectKey();
- verify(notification).getChangeAuthor();
- verify(notification).getNewResolution();
- verifyNoMoreInteractions(notification);
- });
- }
-
- @DataProvider
- public static Object[][] notFPorWontFixResolution() {
- return new Object[][] {
- {""},
- {randomAlphabetic(9)},
- {Issue.RESOLUTION_FIXED},
- {Issue.RESOLUTION_REMOVED}
- };
- }
-
- @Test
- @UseDataProvider("FPorWontFixResolution")
- public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_FPorWontFix_notifications(String newResolution) {
- String projectKey1 = randomAlphabetic(10);
- String changeAuthor1 = randomAlphabetic(11);
- String projectKey2 = randomAlphabetic(12);
- String changeAuthor2 = randomAlphabetic(13);
- Set<IssueChangeNotification> notifications1 = randomSetOfNotifications(projectKey1, changeAuthor1, newResolution);
- Set<IssueChangeNotification> notifications2 = randomSetOfNotifications(projectKey2, changeAuthor2, newResolution);
- when(emailNotificationChannel.isActivated()).thenReturn(true);
-
- int deliver = underTest.deliver(Stream.concat(notifications1.stream(), notifications2.stream()).collect(toSet()));
-
- assertThat(deliver).isZero();
- verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey1, ALL_MUST_HAVE_ROLE_USER);
- verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey2, ALL_MUST_HAVE_ROLE_USER);
- verifyNoMoreInteractions(notificationManager);
- verify(emailNotificationChannel).isActivated();
- verifyNoMoreInteractions(emailNotificationChannel);
- }
-
- @Test
- @UseDataProvider("FPorWontFixResolution")
- public void deliver_does_not_send_email_request_for_notifications_a_subscriber_is_the_changeAuthor_of(String newResolution) {
- String projectKey = randomAlphabetic(5);
- String subscriber1 = randomAlphabetic(6);
- String subscriber2 = randomAlphabetic(7);
- String subscriber3 = randomAlphabetic(8);
- String otherChangeAuthor = randomAlphabetic(9);
- // subscriber1 is the changeAuthor of some notifications
- Set<IssueChangeNotification> subscriber1Notifications = randomSetOfNotifications(projectKey, subscriber1, newResolution);
- // subscriber2 is the changeAuthor of some notifications
- Set<IssueChangeNotification> subscriber2Notifications = randomSetOfNotifications(projectKey, subscriber2, newResolution);
- // subscriber3 has no notification
- Set<IssueChangeNotification> otherChangeAuthorNotifications = randomSetOfNotifications(projectKey, otherChangeAuthor, newResolution);
- when(emailNotificationChannel.isActivated()).thenReturn(true);
- Set<String> subscribers = ImmutableSet.of(subscriber1, subscriber2, subscriber3);
- when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER))
- .thenReturn(subscribers.stream().map(DoNotFixNotificationHandlerTest::emailRecipientOf).collect(toSet()));
- Set<EmailDeliveryRequest> expectedRequests = Stream.of(
- subscriber1Notifications.stream().flatMap(notif -> Stream.of(subscriber2, subscriber3).map(login -> new EmailDeliveryRequest(emailOf(login), notif))),
- subscriber2Notifications.stream().flatMap(notif -> Stream.of(subscriber1, subscriber3).map(login -> new EmailDeliveryRequest(emailOf(login), notif))),
- otherChangeAuthorNotifications.stream().flatMap(notif -> Stream.of(subscriber1, subscriber2, subscriber3).map(login -> new EmailDeliveryRequest(emailOf(login), notif))))
- .flatMap(t -> t)
- .collect(toSet());
- int deliveredCount = new Random().nextInt(expectedRequests.size());
- when(emailNotificationChannel.deliverAll(expectedRequests)).thenReturn(deliveredCount);
-
- Set<IssueChangeNotification> notifications = Stream.of(
- subscriber1Notifications.stream(),
- subscriber2Notifications.stream(),
- otherChangeAuthorNotifications.stream())
- .flatMap(t -> t)
- .collect(toSet());
- int deliver = underTest.deliver(notifications);
-
- assertThat(deliver).isEqualTo(deliveredCount);
- verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER);
- verifyNoMoreInteractions(notificationManager);
- verify(emailNotificationChannel).isActivated();
- verify(emailNotificationChannel).deliverAll(expectedRequests);
- verifyNoMoreInteractions(emailNotificationChannel);
- }
-
- @DataProvider
- public static Object[][] FPorWontFixResolution() {
- return new Object[][] {
- {Issue.RESOLUTION_FALSE_POSITIVE},
- {Issue.RESOLUTION_WONT_FIX}
- };
- }
-
- private static Set<IssueChangeNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String changeAuthor, @Nullable String newResolution) {
- return IntStream.range(0, 1 + new Random().nextInt(5))
- .mapToObj(i -> newNotification(projectKey, changeAuthor, newResolution))
- .collect(Collectors.toSet());
- }
-
- private static IssueChangeNotification newNotification(@Nullable String projectKey, @Nullable String changeAuthor, @Nullable String newResolution) {
- IssueChangeNotification notification = mock(IssueChangeNotification.class);
- when(notification.getProjectKey()).thenReturn(projectKey);
- when(notification.getChangeAuthor()).thenReturn(changeAuthor);
- when(notification.getNewResolution()).thenReturn(newResolution);
- return notification;
- }
-
- private static NotificationManager.EmailRecipient emailRecipientOf(String assignee1) {
- return new NotificationManager.EmailRecipient(assignee1, emailOf(assignee1));
- }
-
- private static String emailOf(String assignee1) {
- return assignee1 + "@baffe";
- }
-
-}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/EmailMessageTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/EmailMessageTest.java
new file mode 100644
index 00000000000..d2758efd297
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/EmailMessageTest.java
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import org.junit.Test;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EmailMessageTest {
+ private EmailMessage underTest = new EmailMessage();
+
+ @Test
+ public void setHtmlMessage_sets_message_and_html_to_true() {
+ String message = randomAlphabetic(12);
+
+ underTest.setHtmlMessage(message);
+
+ assertThat(underTest.getMessage()).isEqualTo(message);
+ assertThat(underTest.isHtml()).isTrue();
+ }
+
+ @Test
+ public void setPlainTextMessage_sets_message_and_html_to_false() {
+ String message = randomAlphabetic(12);
+
+ underTest.setPlainTextMessage(message);
+
+ assertThat(underTest.getMessage()).isEqualTo(message);
+ assertThat(underTest.isHtml()).isFalse();
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandlerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandlerTest.java
new file mode 100644
index 00000000000..b4775a439dc
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandlerTest.java
@@ -0,0 +1,498 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
+
+import static java.util.Collections.singleton;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
+import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
+import static org.sonar.core.util.stream.MoreCollectors.index;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
+import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
+import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
+
+@RunWith(DataProviderRunner.class)
+public class FPOrWontFixNotificationHandlerTest {
+ private static final String DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY = "NewFalsePositiveIssue";
+ private NotificationManager notificationManager = mock(NotificationManager.class);
+ private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
+ private IssuesChangesNotificationSerializer serializerMock = mock(IssuesChangesNotificationSerializer.class);
+ private IssuesChangesNotificationSerializer serializer = spy(new IssuesChangesNotificationSerializer());
+ private Class<Set<EmailDeliveryRequest>> requestSetType = (Class<Set<EmailDeliveryRequest>>) (Class<?>) Set.class;
+ private FPOrWontFixNotificationHandler underTest = new FPOrWontFixNotificationHandler(notificationManager, emailNotificationChannel, serializer);
+
+ @Test
+ public void getMetadata_returns_same_instance_as_static_method() {
+ assertThat(underTest.getMetadata().get()).isSameAs(FPOrWontFixNotificationHandler.newMetadata());
+ }
+
+ @Test
+ public void verify_fpOrWontFixIssues_notification_dispatcher_key() {
+ NotificationDispatcherMetadata metadata = FPOrWontFixNotificationHandler.newMetadata();
+
+ assertThat(metadata.getDispatcherKey()).isEqualTo(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY);
+ }
+
+ @Test
+ public void fpOrWontFixIssues_notification_is_disabled_at_global_level() {
+ NotificationDispatcherMetadata metadata = FPOrWontFixNotificationHandler.newMetadata();
+
+ assertThat(metadata.getProperty(GLOBAL_NOTIFICATION)).isEqualTo("false");
+ }
+
+ @Test
+ public void fpOrWontFixIssues_notification_is_enable_at_project_level() {
+ NotificationDispatcherMetadata metadata = FPOrWontFixNotificationHandler.newMetadata();
+
+ assertThat(metadata.getProperty(PER_PROJECT_NOTIFICATION)).isEqualTo("true");
+ }
+
+ @Test
+ public void getNotificationClass_is_IssueChangeNotification() {
+ assertThat(underTest.getNotificationClass()).isEqualTo(IssuesChangesNotification.class);
+ }
+
+ @Test
+ public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
+ when(emailNotificationChannel.isActivated()).thenReturn(false);
+ Set<IssuesChangesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
+ .mapToObj(i -> mock(IssuesChangesNotification.class))
+ .collect(toSet());
+
+ int deliver = underTest.deliver(notifications);
+
+ assertThat(deliver).isZero();
+ verifyZeroInteractions(notificationManager);
+ verify(emailNotificationChannel).isActivated();
+ verifyNoMoreInteractions(emailNotificationChannel);
+ notifications.forEach(Mockito::verifyZeroInteractions);
+ }
+
+ @Test
+ public void deliver_parses_every_notification_in_order() {
+ Set<IssuesChangesNotification> notifications = IntStream.range(0, 5 + new Random().nextInt(10))
+ .mapToObj(i -> mock(IssuesChangesNotification.class))
+ .collect(toSet());
+ when(emailNotificationChannel.isActivated()).thenReturn(true);
+ when(serializerMock.from(any(IssuesChangesNotification.class))).thenReturn(mock(IssuesChangesNotificationBuilder.class));
+ FPOrWontFixNotificationHandler underTest = new FPOrWontFixNotificationHandler(notificationManager, emailNotificationChannel, serializerMock);
+
+ underTest.deliver(notifications);
+
+ notifications.forEach(notification -> verify(serializerMock).from(notification));
+ }
+
+ @Test
+ public void deliver_fails_with_IAE_if_serializer_throws_IAE() {
+ Set<IssuesChangesNotification> notifications = IntStream.range(0, 3 + new Random().nextInt(10))
+ .mapToObj(i -> mock(IssuesChangesNotification.class))
+ .collect(toSet());
+ when(emailNotificationChannel.isActivated()).thenReturn(true);
+ IllegalArgumentException expected = new IllegalArgumentException("faking serializer#from throwing a IllegalArgumentException");
+ when(serializerMock.from(any(IssuesChangesNotification.class)))
+ .thenReturn(mock(IssuesChangesNotificationBuilder.class))
+ .thenReturn(mock(IssuesChangesNotificationBuilder.class))
+ .thenThrow(expected);
+ FPOrWontFixNotificationHandler underTest = new FPOrWontFixNotificationHandler(notificationManager, emailNotificationChannel, serializerMock);
+
+ try {
+ underTest.deliver(notifications);
+ fail("should have throws IAE");
+ } catch (IllegalArgumentException e) {
+ verify(serializerMock, times(3)).from(any(IssuesChangesNotification.class));
+ assertThat(e).isSameAs(expected);
+ }
+ }
+
+ @Test
+ public void deliver_has_no_effect_if_no_issue_has_new_resolution() {
+ when(emailNotificationChannel.isActivated()).thenReturn(true);
+ Change changeMock = mock(Change.class);
+ Set<IssuesChangesNotification> notifications = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setNewResolution(null)).collect(toSet()), changeMock))
+ .map(serializer::serialize)
+ .collect(toSet());
+ reset(serializer);
+
+ int deliver = underTest.deliver(notifications);
+
+ assertThat(deliver).isZero();
+ verify(serializer, times(notifications.size())).from(any(IssuesChangesNotification.class));
+ verifyZeroInteractions(changeMock);
+ verifyNoMoreInteractions(serializer);
+ verifyZeroInteractions(notificationManager);
+ verify(emailNotificationChannel).isActivated();
+ verifyNoMoreInteractions(emailNotificationChannel);
+ }
+
+ @Test
+ @UseDataProvider("notFPorWontFixResolution")
+ public void deliver_has_no_effect_if_no_issue_has_FP_or_wontfix_resolution(String newResolution) {
+ when(emailNotificationChannel.isActivated()).thenReturn(true);
+ Change changeMock = mock(Change.class);
+ Set<IssuesChangesNotification> notifications = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setNewResolution(newResolution)).collect(toSet()), changeMock))
+ .map(serializer::serialize)
+ .collect(toSet());
+ reset(serializer);
+
+ int deliver = underTest.deliver(notifications);
+
+ assertThat(deliver).isZero();
+ verify(serializer, times(notifications.size())).from(any(IssuesChangesNotification.class));
+ verifyZeroInteractions(changeMock);
+ verifyNoMoreInteractions(serializer);
+ verifyZeroInteractions(notificationManager);
+ verify(emailNotificationChannel).isActivated();
+ verifyNoMoreInteractions(emailNotificationChannel);
+ }
+
+ @DataProvider
+ public static Object[][] notFPorWontFixResolution() {
+ return new Object[][] {
+ {""},
+ {randomAlphabetic(9)},
+ {Issue.RESOLUTION_FIXED},
+ {Issue.RESOLUTION_REMOVED}
+ };
+ }
+
+ @Test
+ @UseDataProvider("FPorWontFixResolution")
+ public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_FPorWontFix_notifications(String newResolution) {
+ Project projectKey1 = newProject(randomAlphabetic(4));
+ Project projectKey2 = newProject(randomAlphabetic(5));
+ Project projectKey3 = newProject(randomAlphabetic(6));
+ Project projectKey4 = newProject(randomAlphabetic(7));
+ Change changeMock = mock(Change.class);
+ // some notifications with some issues on project1
+ Stream<IssuesChangesNotificationBuilder> project1Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(
+ randomIssues(t -> t.setProject(projectKey1).setNewResolution(newResolution)).collect(toSet()),
+ changeMock));
+ // some notifications with some issues on project2
+ Stream<IssuesChangesNotificationBuilder> project2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(
+ randomIssues(t -> t.setProject(projectKey2).setNewResolution(newResolution)).collect(toSet()),
+ changeMock));
+ // some notifications with some issues on project3 and project 4
+ Stream<IssuesChangesNotificationBuilder> project3And4Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(
+ Stream.concat(
+ randomIssues(t -> t.setProject(projectKey3).setNewResolution(newResolution)),
+ randomIssues(t -> t.setProject(projectKey4).setNewResolution(newResolution)))
+ .collect(toSet()),
+ changeMock));
+ when(emailNotificationChannel.isActivated()).thenReturn(true);
+
+ Set<IssuesChangesNotification> notifications = Stream.of(project1Notifications, project2Notifications, project3And4Notifications)
+ .flatMap(t -> t)
+ .map(serializer::serialize)
+ .collect(toSet());
+ int deliver = underTest.deliver(notifications);
+
+ assertThat(deliver).isZero();
+ verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey1.getKey(), ALL_MUST_HAVE_ROLE_USER);
+ verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey2.getKey(), ALL_MUST_HAVE_ROLE_USER);
+ verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey3.getKey(), ALL_MUST_HAVE_ROLE_USER);
+ verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey4.getKey(), ALL_MUST_HAVE_ROLE_USER);
+ verifyNoMoreInteractions(notificationManager);
+ verify(emailNotificationChannel).isActivated();
+ verifyNoMoreInteractions(emailNotificationChannel);
+ verifyZeroInteractions(changeMock);
+ }
+
+ @Test
+ @UseDataProvider("FPorWontFixResolution")
+ public void deliver_does_not_send_email_request_for_notifications_a_subscriber_is_the_changeAuthor_of(String newResolution) {
+ Project project = newProject(randomAlphabetic(5));
+ User subscriber1 = newUser("subscriber1");
+ User subscriber2 = newUser("subscriber2");
+ User subscriber3 = newUser("subscriber3");
+ User otherChangeAuthor = newUser("otherChangeAuthor");
+
+ // subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 only
+ Set<IssuesChangesNotificationBuilder> subscriber1Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(
+ randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)).collect(toSet()),
+ newUserChange(subscriber1)))
+ .collect(toSet());
+ // subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 and subscriber2
+ Set<IssuesChangesNotificationBuilder> subscriber1and2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(
+ Stream.concat(
+ randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)),
+ randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber1)))
+ .collect(toSet()),
+ newUserChange(subscriber1)))
+ .collect(toSet());
+ // subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 only
+ Set<IssuesChangesNotificationBuilder> subscriber2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(
+ randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)).collect(toSet()),
+ newUserChange(subscriber2)))
+ .collect(toSet());
+ // subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 and subscriber 3
+ Set<IssuesChangesNotificationBuilder> subscriber2And3Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(
+ Stream.concat(
+ randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)),
+ randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber3)))
+ .collect(toSet()),
+ newUserChange(subscriber2)))
+ .collect(toSet());
+ // subscriber3 is the changeAuthor of no notification
+ // otherChangeAuthor has some notifications
+ Set<IssuesChangesNotificationBuilder> otherChangeAuthorNotifications = IntStream.range(0, 1 + new Random().nextInt(2))
+ .mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setProject(project).setNewResolution(newResolution)).collect(toSet()),
+ newUserChange(otherChangeAuthor)))
+ .collect(toSet());
+ when(emailNotificationChannel.isActivated()).thenReturn(true);
+
+ Set<String> subscriberLogins = ImmutableSet.of(subscriber1.getLogin(), subscriber2.getLogin(), subscriber3.getLogin());
+ when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
+ .thenReturn(subscriberLogins.stream().map(FPOrWontFixNotificationHandlerTest::emailRecipientOf).collect(toSet()));
+
+ int deliveredCount = new Random().nextInt(200);
+ when(emailNotificationChannel.deliverAll(anySet()))
+ .thenReturn(deliveredCount)
+ .thenThrow(new IllegalStateException("deliver should be called only once"));
+
+ Set<IssuesChangesNotification> notifications = Stream.of(
+ subscriber1Notifications.stream(),
+ subscriber1and2Notifications.stream(),
+ subscriber2Notifications.stream(),
+ subscriber2And3Notifications.stream(),
+ otherChangeAuthorNotifications.stream())
+ .flatMap(t -> t)
+ .map(serializer::serialize)
+ .collect(toSet());
+ reset(serializer);
+
+ int deliver = underTest.deliver(notifications);
+
+ assertThat(deliver).isEqualTo(deliveredCount);
+ verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER);
+ verifyNoMoreInteractions(notificationManager);
+ verify(emailNotificationChannel).isActivated();
+ ArgumentCaptor<Set<EmailDeliveryRequest>> captor = ArgumentCaptor.forClass(requestSetType);
+ verify(emailNotificationChannel).deliverAll(captor.capture());
+ verifyNoMoreInteractions(emailNotificationChannel);
+ ListMultimap<String, EmailDeliveryRequest> requestsByRecipientEmail = captor.getValue().stream()
+ .collect(index(EmailDeliveryRequest::getRecipientEmail));
+ assertThat(requestsByRecipientEmail.get(emailOf(subscriber1.getLogin())))
+ .containsOnly(
+ Stream.of(
+ subscriber2Notifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrWontFix(newResolution))),
+ subscriber2And3Notifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrWontFix(newResolution))),
+ otherChangeAuthorNotifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrWontFix(newResolution))))
+ .flatMap(t -> t)
+ .toArray(EmailDeliveryRequest[]::new));
+ assertThat(requestsByRecipientEmail.get(emailOf(subscriber2.getLogin())))
+ .containsOnly(
+ Stream.of(
+ subscriber1Notifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrWontFix(newResolution))),
+ subscriber1and2Notifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrWontFix(newResolution))),
+ otherChangeAuthorNotifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrWontFix(newResolution))))
+ .flatMap(t -> t)
+ .toArray(EmailDeliveryRequest[]::new));
+ assertThat(requestsByRecipientEmail.get(emailOf(subscriber3.getLogin())))
+ .containsOnly(
+ Stream.of(
+ subscriber1Notifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))),
+ subscriber1and2Notifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))),
+ subscriber2Notifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))),
+ subscriber2And3Notifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))),
+ otherChangeAuthorNotifications.stream()
+ .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))))
+ .flatMap(t -> t)
+ .toArray(EmailDeliveryRequest[]::new));
+ assertThat(requestsByRecipientEmail.get(emailOf(otherChangeAuthor.getLogin())))
+ .isEmpty();
+ }
+
+ @Test
+ @UseDataProvider("oneOrMoreProjectCounts")
+ public void deliver_send_a_separated_email_request_for_FPs_and_Wont_Fix_issues(int projectCount) {
+ Set<Project> projects = IntStream.range(0, projectCount).mapToObj(i -> newProject("prk_key_" + i)).collect(toSet());
+ User subscriber1 = newUser("subscriber1");
+ User changeAuthor = newUser("changeAuthor");
+
+ Set<ChangedIssue> fpIssues = projects.stream()
+ .flatMap(project -> randomIssues(t -> t.setProject(project).setNewResolution(RESOLUTION_FALSE_POSITIVE).setAssignee(subscriber1)))
+ .collect(toSet());
+ Set<ChangedIssue> wontFixIssues = projects.stream()
+ .flatMap(project -> randomIssues(t -> t.setProject(project).setNewResolution(RESOLUTION_WONT_FIX).setAssignee(subscriber1)))
+ .collect(toSet());
+ UserChange userChange = newUserChange(changeAuthor);
+ IssuesChangesNotificationBuilder fpAndWontFixNotifications = new IssuesChangesNotificationBuilder(
+ Stream.concat(fpIssues.stream(), wontFixIssues.stream()).collect(toSet()),
+ userChange);
+ when(emailNotificationChannel.isActivated()).thenReturn(true);
+ projects.forEach(project -> when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
+ .thenReturn(singleton(emailRecipientOf(subscriber1.getLogin()))));
+
+ int deliveredCount = new Random().nextInt(200);
+ when(emailNotificationChannel.deliverAll(anySet()))
+ .thenReturn(deliveredCount)
+ .thenThrow(new IllegalStateException("deliver should be called only once"));
+ Set<IssuesChangesNotification> notifications = singleton(serializer.serialize(fpAndWontFixNotifications));
+ reset(serializer);
+
+ int deliver = underTest.deliver(notifications);
+
+ assertThat(deliver).isEqualTo(deliveredCount);
+ projects
+ .forEach(project -> verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER));
+ verifyNoMoreInteractions(notificationManager);
+ verify(emailNotificationChannel).isActivated();
+ ArgumentCaptor<Set<EmailDeliveryRequest>> captor = ArgumentCaptor.forClass(requestSetType);
+ verify(emailNotificationChannel).deliverAll(captor.capture());
+ verifyNoMoreInteractions(emailNotificationChannel);
+ ListMultimap<String, EmailDeliveryRequest> requestsByRecipientEmail = captor.getValue().stream()
+ .collect(index(EmailDeliveryRequest::getRecipientEmail));
+ assertThat(requestsByRecipientEmail.get(emailOf(subscriber1.getLogin())))
+ .containsOnly(
+ new EmailDeliveryRequest(emailOf(subscriber1.getLogin()), new FPOrWontFixNotification(
+ userChange, wontFixIssues, FpOrWontFix.WONT_FIX)),
+ new EmailDeliveryRequest(emailOf(subscriber1.getLogin()), new FPOrWontFixNotification(
+ userChange, fpIssues, FpOrWontFix.FP)));
+ }
+
+ @DataProvider
+ public static Object[][] oneOrMoreProjectCounts() {
+ return new Object[][] {
+ {1},
+ {2 + new Random().nextInt(3)},
+ };
+ }
+
+ private static EmailDeliveryRequest newEmailDeliveryRequest(IssuesChangesNotificationBuilder notif, User user, FpOrWontFix resolution) {
+ return new EmailDeliveryRequest(
+ emailOf(user.getLogin()),
+ new FPOrWontFixNotification(notif.getChange(), notif.getIssues(), resolution));
+ }
+
+ private static FpOrWontFix toFpOrWontFix(String newResolution) {
+ if (newResolution.equals(Issue.RESOLUTION_WONT_FIX)) {
+ return FpOrWontFix.WONT_FIX;
+ }
+ if (newResolution.equals(RESOLUTION_FALSE_POSITIVE)) {
+ return FpOrWontFix.FP;
+ }
+ throw new IllegalArgumentException("unsupported resolution " + newResolution);
+ }
+
+ private static long counter = 233_343;
+
+ private static UserChange newUserChange(User subscriber1) {
+ return new UserChange(counter += 100, subscriber1);
+ }
+
+ public User newUser(String subscriber1) {
+ return new User(subscriber1, subscriber1 + "_login", subscriber1 + "_name");
+ }
+
+ @DataProvider
+ public static Object[][] FPorWontFixResolution() {
+ return new Object[][] {
+ {RESOLUTION_FALSE_POSITIVE},
+ {Issue.RESOLUTION_WONT_FIX}
+ };
+ }
+
+ private static Stream<ChangedIssue> randomIssues(Consumer<ChangedIssue.Builder> consumer) {
+ return IntStream.range(0, 1 + new Random().nextInt(5))
+ .mapToObj(i -> {
+ ChangedIssue.Builder builder = new ChangedIssue.Builder("key_" + i)
+ .setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5)))
+ .setNewStatus(randomAlphabetic(12))
+ .setNewResolution(randomAlphabetic(13))
+ .setRule(new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), randomAlphabetic(8)))
+ .setProject(new Project.Builder(randomAlphabetic(9))
+ .setKey(randomAlphabetic(10))
+ .setProjectName(randomAlphabetic(11))
+ .build());
+ consumer.accept(builder);
+ return builder.build();
+ });
+ }
+
+ private static NotificationManager.EmailRecipient emailRecipientOf(String assignee1) {
+ return new NotificationManager.EmailRecipient(assignee1, emailOf(assignee1));
+ }
+
+ private static String emailOf(String assignee1) {
+ return assignee1 + "@baffe";
+ }
+
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationTest.java
new file mode 100644
index 00000000000..cd9145704a3
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationTest.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Test;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP;
+import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX;
+
+public class FPOrWontFixNotificationTest {
+ @Test
+ public void equals_is_based_on_issues_change_and_resolution() {
+ Rule rule = new Rule(RuleKey.of("repo", "rule_key"), "rule_name");
+ Project project = new Project.Builder("prj_uuid").setKey("prj_key").setProjectName("prj_name").build();
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(i -> new ChangedIssue.Builder("key_" + i)
+ .setNewStatus("status")
+ .setRule(rule)
+ .setProject(project)
+ .build())
+ .collect(Collectors.toSet());
+ AnalysisChange change = new AnalysisChange(12);
+ User user = new User("uuid", "login", null);
+ FPOrWontFixNotification underTest = new FPOrWontFixNotification(change, changedIssues, WONT_FIX);
+
+ assertThat(underTest)
+ .isEqualTo(new FPOrWontFixNotification(change, changedIssues, WONT_FIX))
+ .isEqualTo(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), WONT_FIX))
+ .isNotEqualTo(new Object())
+ .isNotEqualTo(null)
+ .isNotEqualTo(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX))
+ .isNotEqualTo(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssues.iterator().next()), WONT_FIX))
+ .isNotEqualTo(new FPOrWontFixNotification(new AnalysisChange(14), changedIssues, WONT_FIX))
+ .isNotEqualTo(new FPOrWontFixNotification(new IssuesChangesNotificationBuilder.UserChange(12, user), changedIssues, WONT_FIX))
+ .isNotEqualTo(new FPOrWontFixNotification(change, changedIssues, FP));
+ }
+ @Test
+ public void hashcode_is_based_on_issues_change_and_resolution() {
+ Rule rule = new Rule(RuleKey.of("repo", "rule_key"), "rule_name");
+ Project project = new Project.Builder("prj_uuid").setKey("prj_key").setProjectName("prj_name").build();
+ Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(i -> new ChangedIssue.Builder("key_" + i)
+ .setNewStatus("status")
+ .setRule(rule)
+ .setProject(project)
+ .build())
+ .collect(Collectors.toSet());
+ AnalysisChange change = new AnalysisChange(12);
+ User user = new User("uuid", "login", null);
+ FPOrWontFixNotification underTest = new FPOrWontFixNotification(change, changedIssues, WONT_FIX);
+
+ assertThat(underTest.hashCode())
+ .isEqualTo(new FPOrWontFixNotification(change, changedIssues, WONT_FIX).hashCode())
+ .isEqualTo(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), WONT_FIX).hashCode())
+ .isNotEqualTo(new Object().hashCode())
+ .isNotEqualTo(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX).hashCode())
+ .isNotEqualTo(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssues.iterator().next()), WONT_FIX).hashCode())
+ .isNotEqualTo(new FPOrWontFixNotification(new AnalysisChange(14), changedIssues, WONT_FIX).hashCode())
+ .isNotEqualTo(new FPOrWontFixNotification(new IssuesChangesNotificationBuilder.UserChange(12, user), changedIssues, WONT_FIX).hashCode())
+ .isNotEqualTo(new FPOrWontFixNotification(change, changedIssues, FP)).hashCode();
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplateTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplateTest.java
new file mode 100644
index 00000000000..038a65d7c6c
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplateTest.java
@@ -0,0 +1,421 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import com.google.common.collect.ImmutableSet;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
+import org.sonar.test.html.HtmlFragmentAssert;
+
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP;
+import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX;
+
+@RunWith(DataProviderRunner.class)
+public class FpOrWontFixEmailTemplateTest {
+ private I18n i18n = mock(I18n.class);
+ private EmailSettings emailSettings = mock(EmailSettings.class);
+ private FpOrWontFixEmailTemplate underTest = new FpOrWontFixEmailTemplate(i18n, emailSettings);
+
+ @Test
+ public void format_returns_null_on_Notification() {
+ EmailMessage emailMessage = underTest.format(mock(Notification.class));
+
+ assertThat(emailMessage).isNull();
+ }
+
+ @Test
+ public void format_sets_message_id_specific_to_fp() {
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(mock(Change.class), Collections.emptySet(), FP));
+
+ assertThat(emailMessage.getMessageId()).isEqualTo("fp-issue-changes");
+ }
+
+ @Test
+ public void format_sets_message_id_specific_to_wont_fix() {
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(mock(Change.class), Collections.emptySet(), WONT_FIX));
+
+ assertThat(emailMessage.getMessageId()).isEqualTo("wontfix-issue-changes");
+ }
+
+ @Test
+ public void format_sets_subject_specific_to_fp() {
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(mock(Change.class), Collections.emptySet(), FP));
+
+ assertThat(emailMessage.getSubject()).isEqualTo("Issues marked as False Positive");
+ }
+
+ @Test
+ public void format_sets_subject_specific_to_wont_fix() {
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(mock(Change.class), Collections.emptySet(), WONT_FIX));
+
+ assertThat(emailMessage.getSubject()).isEqualTo("Issues marked as Won't Fix");
+ }
+
+ @Test
+ public void format_sets_from_to_name_of_author_change_when_available() {
+ UserChange change = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6), randomAlphabetic(7)));
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX));
+
+ assertThat(emailMessage.getFrom()).isEqualTo(change.getUser().getName().get());
+ }
+
+ @Test
+ public void format_sets_from_to_login_of_author_change_when_name_is_not_available() {
+ UserChange change = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6), null));
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX));
+
+ assertThat(emailMessage.getFrom()).isEqualTo(change.getUser().getLogin());
+ }
+
+ @Test
+ public void format_sets_from_to_null_when_analysisChange() {
+ AnalysisChange change = new AnalysisChange(new Random().nextLong());
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX));
+
+ assertThat(emailMessage.getFrom()).isNull();
+ }
+
+ @Test
+ @UseDataProvider("userOrAnalysisChange")
+ public void formats_returns_html_message_with_only_footer_and_header_when_no_issue_for_FPs(Change change) {
+ formats_returns_html_message_with_only_footer_and_header_when_no_issue(change, FP, "False Positive");
+ }
+
+ @Test
+ @UseDataProvider("userOrAnalysisChange")
+ public void formats_returns_html_message_with_only_footer_and_header_when_no_issue_for_Wont_fixs(Change change) {
+ formats_returns_html_message_with_only_footer_and_header_when_no_issue(change, WONT_FIX, "Won't Fix");
+ }
+
+ public void formats_returns_html_message_with_only_footer_and_header_when_no_issue(Change change, FpOrWontFix fpOrWontFix, String fpOrWontFixLabel) {
+ String wordingNotification = randomAlphabetic(20);
+ String host = randomAlphabetic(15);
+ String instance = randomAlphabetic(17);
+ when(i18n.message(Locale.ENGLISH, "notification.dispatcher.NewFalsePositiveIssue", "notification.dispatcher.NewFalsePositiveIssue"))
+ .thenReturn(wordingNotification);
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+ when(emailSettings.getInstanceName()).thenReturn(instance);
+
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, Collections.emptySet(), fpOrWontFix));
+
+ String footerText = "You received this email because you are subscribed to \"" + wordingNotification + "\" notifications from " + instance + "."
+ + " Click here to edit your email preferences.";
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph("Hi,")
+ .withoutLink()
+ .hasParagraph("A manual change has resolved an issue as " + fpOrWontFixLabel + ":")
+ .withoutLink()
+ .hasEmptyParagraph()
+ .hasParagraph(footerText)
+ .withSmallOn(footerText)
+ .withLink("here", host + "/account/notifications")
+ .noMoreBlock();
+ }
+
+ @Test
+ @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+ public void formats_returns_html_message_for_single_issue_on_master(Change change, FpOrWontFix fpOrWontFix) {
+ Project project = newProject("1");
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ ChangedIssue changedIssue = newChangedIssue("key", project, ruleName);
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName())
+ .hasList("Rule " + ruleName + " - See the single issue")
+ .withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+ public void formats_returns_html_message_for_single_issue_on_branch(Change change, FpOrWontFix fpOrWontFix) {
+ String branchName = randomAlphabetic(6);
+ Project project = newBranch("1", branchName);
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ String key = "key";
+ ChangedIssue changedIssue = newChangedIssue(key, project, ruleName);
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName() + ", " + branchName)
+ .hasList("Rule " + ruleName + " - See the single issue")
+ .withLink("See the single issue",
+ host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+ public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master(Change change, FpOrWontFix fpOrWontFix) {
+ Project project = newProject("1");
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ Rule rule = newRule(ruleName);
+ List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
+ .collect(toList());
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix));
+
+ String expectedHref = host + "/project/issues?id=" + project.getKey()
+ + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
+ String expectedLinkText = "See all " + changedIssues.size() + " issues";
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName())
+ .hasList("Rule " + ruleName + " - " + expectedLinkText)
+ .withLink(expectedLinkText, expectedHref)
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+ public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch(Change change, FpOrWontFix fpOrWontFix) {
+ String branchName = randomAlphabetic(19);
+ Project project = newBranch("1", branchName);
+ String ruleName = randomAlphabetic(8);
+ String host = randomAlphabetic(15);
+ Rule rule = newRule(ruleName);
+ List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+ .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
+ .collect(toList());
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix));
+
+ String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
+ + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
+ String expectedLinkText = "See all " + changedIssues.size() + " issues";
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName() + ", " + branchName)
+ .hasList("Rule " + ruleName + " - " + expectedLinkText)
+ .withLink(expectedLinkText, expectedHref)
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+ public void formats_returns_html_message_with_projects_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) {
+ Project project1 = newProject("1");
+ Project project1Branch1 = newBranch("1", "a");
+ Project project1Branch2 = newBranch("1", "b");
+ Project project2 = newProject("B");
+ Project project2Branch1 = newBranch("B", "a");
+ Project project3 = newProject("C");
+ String host = randomAlphabetic(15);
+ List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3)
+ .map(project -> newChangedIssue("issue_" + project.getUuid(), project, newRule(randomAlphabetic(2))))
+ .collect(toList());
+ Collections.shuffle(changedIssues);
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project1.getProjectName())
+ .hasList()
+ .hasParagraph(project1Branch1.getProjectName() + ", " + project1Branch1.getBranchName().get())
+ .hasList()
+ .hasParagraph(project1Branch2.getProjectName() + ", " + project1Branch2.getBranchName().get())
+ .hasList()
+ .hasParagraph(project2.getProjectName())
+ .hasList()
+ .hasParagraph(project2Branch1.getProjectName() + ", " + project2Branch1.getBranchName().get())
+ .hasList()
+ .hasParagraph(project3.getProjectName())
+ .hasList()
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+ public void formats_returns_html_message_with_rules_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) {
+ Project project = newProject("1");
+ Rule rule1 = newRule("1");
+ Rule rule2 = newRule("a");
+ Rule rule3 = newRule("b");
+ Rule rule4 = newRule("X");
+ String host = randomAlphabetic(15);
+ List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
+ .map(rule -> newChangedIssue("issue_" + rule.getName(), project, rule))
+ .collect(toList());
+ Collections.shuffle(changedIssues);
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project.getProjectName())
+ .hasList(
+ "Rule " + rule1.getName() + " - See the single issue",
+ "Rule " + rule2.getName() + " - See the single issue",
+ "Rule " + rule3.getName() + " - See the single issue",
+ "Rule " + rule4.getName() + " - See the single issue")
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @Test
+ @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+ public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues(Change change, FpOrWontFix fpOrWontFix) {
+ Project project1 = newProject("1");
+ Project project2 = newProject("V");
+ Project project2Branch = newBranch("V", "AB");
+ Rule rule1 = newRule("1");
+ Rule rule2 = newRule("a");
+ String host = randomAlphabetic(15);
+ List<ChangedIssue> changedIssues = Stream.of(
+ IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, project1, rule1)),
+ IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, project1, rule2)),
+ IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, project2, rule2)),
+ IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, project2Branch, rule1)))
+ .flatMap(t -> t)
+ .collect(toList());
+ Collections.shuffle(changedIssues);
+ when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+ EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix));
+
+ HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+ .hasParagraph().hasParagraph() // skip header
+ .hasParagraph(project1.getProjectName())
+ .hasList()
+ .withItemTexts(
+ "Rule " + rule1.getName() + " - See all 39 issues",
+ "Rule " + rule2.getName() + " - See all 40 issues")
+ .withLink("See all 39 issues",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
+ .withLink("See all 40 issues",
+ host + "/project/issues?id=" + project1.getKey()
+ + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
+ .hasParagraph(project2.getProjectName())
+ .hasList("Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
+ .withLink("1-40",
+ host + "/project/issues?id=" + project2.getKey()
+ + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
+ .withLink("41-80",
+ host + "/project/issues?id=" + project2.getKey()
+ + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
+ .withLink("81",
+ host + "/project/issues?id=" + project2.getKey()
+ + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
+ .hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get())
+ .hasList("Rule " + rule1.getName() + " - See all 6 issues")
+ .withLink("See all 6 issues",
+ host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
+ + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
+ .hasParagraph().hasParagraph() // skip footer
+ .noMoreBlock();
+ }
+
+ @DataProvider
+ public static Object[][] userOrAnalysisChange() {
+ AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
+ UserChange userChange = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6),
+ new Random().nextBoolean() ? null : randomAlphabetic(7)));
+ return new Object[][] {
+ {analysisChange},
+ {userChange}
+ };
+ }
+
+ @DataProvider
+ public static Object[][] fpOrWontFixValuesByUserOrAnalysisChange() {
+ AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
+ UserChange userChange = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6),
+ new Random().nextBoolean() ? null : randomAlphabetic(7)));
+ return new Object[][] {
+ {analysisChange, FP},
+ {analysisChange, WONT_FIX},
+ {userChange, FP},
+ {userChange, WONT_FIX}
+ };
+ }
+
+ private static ChangedIssue newChangedIssue(String key, Project project, String ruleName) {
+ return newChangedIssue(key, project, newRule(ruleName));
+ }
+
+ private static ChangedIssue newChangedIssue(String key, Project project, Rule rule) {
+ return new ChangedIssue.Builder(key)
+ .setNewStatus(randomAlphabetic(19))
+ .setProject(project)
+ .setRule(rule)
+ .build();
+ }
+
+ private static Rule newRule(String ruleName) {
+ return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleName);
+ }
+
+ private static Project newProject(String uuid) {
+ return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").build();
+ }
+
+ private static Project newBranch(String uuid, String branchName) {
+ return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").setBranchName(branchName).build();
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssueChangeNotificationTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssueChangeNotificationTest.java
deleted file mode 100644
index 5ac3675aa3e..00000000000
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssueChangeNotificationTest.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.server.issue.notification;
-
-import org.junit.Test;
-import org.sonar.core.issue.DefaultIssue;
-import org.sonar.core.issue.FieldDiffs;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.db.user.UserDto;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.db.user.UserTesting.newUserDto;
-
-public class IssueChangeNotificationTest {
-
- private IssueChangeNotification notification = new IssueChangeNotification();
-
- @Test
- public void getProjectKey_returns_null_when_project_is_not_set() {
- assertThat(notification.getProjectKey()).isNull();
- }
-
- @Test
- public void getChangeAuthor_returns_null_when_issue_is_not_set() {
- assertThat(notification.getChangeAuthor()).isNull();
- }
-
- @Test
- public void getNewResolution_returns_null_when_issue_is_not_set() {
- assertThat(notification.getNewResolution()).isNull();
- }
-
- @Test
- public void set_issue() {
- UserDto assignee = newUserDto();
-
- DefaultIssue issue = new DefaultIssue()
- .setKey("ABCD")
- .setAssigneeUuid(assignee.getUuid())
- .setMessage("Remove this useless method")
- .setComponentKey("MyService")
- .setCurrentChange(new FieldDiffs().setDiff("resolution", "FALSE-POSITIVE", "FIXED"));
-
- IssueChangeNotification result = notification.setIssue(issue).setAssignee(assignee);
-
- assertThat(result.getFieldValue("key")).isEqualTo("ABCD");
- assertThat(result.getFieldValue("message")).isEqualTo("Remove this useless method");
- assertThat(result.getFieldValue("old.resolution")).isEqualTo("FALSE-POSITIVE");
- assertThat(result.getFieldValue("new.resolution"))
- .isEqualTo("FIXED")
- .isEqualTo(result.getNewResolution());
- assertThat(result.getFieldValue("assignee")).isEqualTo(assignee.getLogin());
- }
-
- @Test
- public void set_issue_with_current_change_having_no_old_value() {
- DefaultIssue issue = new DefaultIssue()
- .setKey("ABCD")
- .setAssigneeUuid("simon")
- .setMessage("Remove this useless method")
- .setComponentKey("MyService");
-
- IssueChangeNotification result = notification.setIssue(issue.setCurrentChange(new FieldDiffs().setDiff("resolution", null, "FIXED")));
- assertThat(result.getFieldValue("old.resolution")).isNull();
- assertThat(result.getFieldValue("new.resolution"))
- .isEqualTo("FIXED")
- .isEqualTo(result.getNewResolution());
-
- result = notification.setIssue(issue.setCurrentChange(new FieldDiffs().setDiff("resolution", "", "FIXED")));
- assertThat(result.getFieldValue("old.resolution")).isNull();
- assertThat(result.getFieldValue("new.resolution"))
- .isEqualTo("FIXED")
- .isEqualTo(result.getNewResolution());
- }
-
- @Test
- public void set_issue_with_current_change_having_no_new_value() {
- DefaultIssue issue = new DefaultIssue()
- .setKey("ABCD")
- .setAssigneeUuid("simon")
- .setMessage("Remove this useless method")
- .setComponentKey("MyService");
-
- IssueChangeNotification result = notification.setIssue(issue.setCurrentChange(new FieldDiffs().setDiff("assignee", "john", null)));
- assertThat(result.getFieldValue("old.assignee")).isEqualTo("john");
- assertThat(result.getFieldValue("new.assignee")).isNull();
- assertThat(result.getNewResolution()).isNull();
-
- result = notification.setIssue(issue.setCurrentChange(new FieldDiffs().setDiff("assignee", "john", "")));
- assertThat(result.getFieldValue("old.assignee")).isEqualTo("john");
- assertThat(result.getFieldValue("new.assignee")).isNull();
- assertThat(result.getNewResolution()).isNull();
- }
-
- @Test
- public void set_project_without_branch() {
- IssueChangeNotification result = notification.setProject("MyService", "My Service", null, null);
- assertThat(result.getFieldValue("projectKey"))
- .isEqualTo("MyService")
- .isEqualTo(result.getProjectKey());
- assertThat(result.getFieldValue("projectName")).isEqualTo("My Service");
- assertThat(result.getFieldValue("branch")).isNull();
- }
-
- @Test
- public void set_project_with_branch() {
- IssueChangeNotification result = notification.setProject("MyService", "My Service", "feature1", null);
- assertThat(result.getFieldValue("projectKey"))
- .isEqualTo("MyService")
- .isEqualTo(result.getProjectKey());
- assertThat(result.getFieldValue("projectName")).isEqualTo("My Service");
- assertThat(result.getFieldValue("branch")).isEqualTo("feature1");
- }
-
- @Test
- public void set_project_with_pull_request() {
- IssueChangeNotification result = notification.setProject("MyService", "My Service", null, "pr-123");
- assertThat(result.getFieldValue("projectKey"))
- .isEqualTo("MyService")
- .isEqualTo(result.getProjectKey());
- assertThat(result.getFieldValue("projectName")).isEqualTo("My Service");
- assertThat(result.getFieldValue("pullRequest")).isEqualTo("pr-123");
- }
-
- @Test
- public void set_component() {
- IssueChangeNotification result = notification.setComponent(new ComponentDto().setDbKey("MyService").setLongName("My Service"));
- assertThat(result.getFieldValue("componentName")).isEqualTo("My Service");
- assertThat(result.getFieldValue("componentKey")).isEqualTo("MyService");
- }
-
- @Test
- public void set_change_author_login() {
- UserDto user = newUserDto();
- IssueChangeNotification result = notification.setChangeAuthor(user);
- assertThat(result.getFieldValue("changeAuthor"))
- .isEqualTo(user.getLogin())
- .isEqualTo(result.getChangeAuthor());
- }
-
- @Test
- public void set_rule_name() {
- IssueChangeNotification result = notification.setRuleName("Xoo Rule");
- assertThat(result.getFieldValue("ruleName")).isEqualTo("Xoo Rule");
- }
-
- @Test
- public void setComment() {
- IssueChangeNotification result = notification.setComment("My comment");
- assertThat(result.getFieldValue("comment")).isEqualTo("My comment");
- }
-}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest.java
deleted file mode 100644
index da6ed26a453..00000000000
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest.java
+++ /dev/null
@@ -1,200 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.server.issue.notification;
-
-import com.google.common.io.Resources;
-import java.nio.charset.StandardCharsets;
-import org.apache.commons.lang.StringUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.config.EmailSettings;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.notifications.Notification;
-import org.sonar.db.DbTester;
-import org.sonar.db.user.UserDto;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.api.CoreProperties.SERVER_BASE_URL;
-
-public class IssueChangesEmailTemplateTest {
-
- @Rule
- public DbTester db = DbTester.create();
-
- private MapSettings settings = new MapSettings().setProperty(SERVER_BASE_URL, "http://nemo.sonarsource.org");
-
- private IssueChangesEmailTemplate underTest = new IssueChangesEmailTemplate(db.getDbClient(), new EmailSettings(settings.asConfig()));
-
- @Test
- public void should_ignore_non_issue_changes() {
- Notification notification = new Notification("other");
- EmailMessage message = underTest.format(notification);
- assertThat(message).isNull();
- }
-
- @Test
- public void email_should_display_assignee_change() throws Exception {
- Notification notification = generateNotification()
- .setFieldValue("old.assignee", "simon")
- .setFieldValue("new.assignee", "louis");
-
- EmailMessage email = underTest.format(notification);
- assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE");
- assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE");
-
- String message = email.getMessage();
- String expected = Resources.toString(Resources.getResource(
- "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt"),
- StandardCharsets.UTF_8);
- expected = StringUtils.remove(expected, '\r');
- assertThat(message).isEqualTo(expected);
- assertThat(email.getFrom()).isNull();
- }
-
- @Test
- public void email_should_display_plan_change() throws Exception {
- Notification notification = generateNotification()
- .setFieldValue("old.actionPlan", null)
- .setFieldValue("new.actionPlan", "ABC 1.0");
-
- EmailMessage email = underTest.format(notification);
- assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE");
- assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE");
-
- String message = email.getMessage();
- String expected = Resources.toString(Resources.getResource(
- "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt"),
- StandardCharsets.UTF_8);
- expected = StringUtils.remove(expected, '\r');
- assertThat(message).isEqualTo(expected);
- assertThat(email.getFrom()).isNull();
- }
-
- @Test
- public void email_should_display_resolution_change() throws Exception {
- Notification notification = generateNotification()
- .setFieldValue("old.resolution", "FALSE-POSITIVE")
- .setFieldValue("new.resolution", "FIXED");
-
- EmailMessage email = underTest.format(notification);
- assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE");
- assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE");
-
- String message = email.getMessage();
- String expected = Resources.toString(Resources.getResource(
- "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_should_display_resolution_change.txt"),
- StandardCharsets.UTF_8);
- expected = StringUtils.remove(expected, '\r');
- assertThat(message).isEqualTo(expected);
- assertThat(email.getFrom()).isNull();
- }
-
- @Test
- public void display_component_key_if_no_component_name() throws Exception {
- Notification notification = generateNotification()
- .setFieldValue("componentName", null);
-
- EmailMessage email = underTest.format(notification);
- assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE");
- assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE");
-
- String message = email.getMessage();
- String expected = Resources.toString(Resources.getResource(
- "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt"),
- StandardCharsets.UTF_8);
- expected = StringUtils.remove(expected, '\r');
- assertThat(message).isEqualTo(expected);
- }
-
- @Test
- public void test_email_with_multiple_changes() throws Exception {
- Notification notification = generateNotification()
- .setFieldValue("comment", "How to fix it?")
- .setFieldValue("old.assignee", "simon")
- .setFieldValue("new.assignee", "louis")
- .setFieldValue("new.resolution", "FALSE-POSITIVE")
- .setFieldValue("new.status", "RESOLVED")
- .setFieldValue("new.type", "BUG")
- .setFieldValue("new.tags", "bug performance");
-
- EmailMessage email = underTest.format(notification);
- assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE");
- assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE");
-
- String message = email.getMessage();
- String expected = Resources.toString(Resources.getResource(
- "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_multiple_changes.txt"), StandardCharsets.UTF_8);
- expected = StringUtils.remove(expected, '\r');
- assertThat(message).isEqualTo(expected);
- assertThat(email.getFrom()).isNull();
- }
-
- @Test
- public void test_email_with_issue_on_branch() throws Exception {
- Notification notification = generateNotification()
- .setFieldValue("branch", "feature1");
-
- EmailMessage email = underTest.format(notification);
- assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE");
- assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE");
-
- String message = email.getMessage();
- String expected = Resources.toString(Resources.getResource(
- "org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_issue_on_branch.txt"),
- StandardCharsets.UTF_8);
- expected = StringUtils.remove(expected, '\r');
- assertThat(message).isEqualTo(expected);
- }
-
- @Test
- public void notification_sender_should_be_the_author_of_change() {
- UserDto user = db.users().insertUser();
-
- Notification notification = new IssueChangeNotification()
- .setChangeAuthor(user)
- .setProject("Struts", "org.apache:struts", null, null);
-
- EmailMessage message = underTest.format(notification);
- assertThat(message.getFrom()).isEqualTo(user.getName());
- }
-
- @Test
- public void notification_contains_user_login_when_user_is_removed() {
- UserDto user = db.users().insertDisabledUser();
-
- Notification notification = new IssueChangeNotification()
- .setChangeAuthor(user)
- .setProject("Struts", "org.apache:struts", null, null);
-
- EmailMessage message = underTest.format(notification);
- assertThat(message.getFrom()).isEqualTo(user.getLogin());
- }
-
- private static Notification generateNotification() {
- return new IssueChangeNotification()
- .setFieldValue("projectName", "Struts")
- .setFieldValue("projectKey", "org.apache:struts")
- .setFieldValue("componentName", "Action")
- .setFieldValue("componentKey", "org.apache.struts.Action")
- .setFieldValue("key", "ABCDE")
- .setFieldValue("ruleName", "Avoid Cycles")
- .setFieldValue("message", "Has 3 cycles");
- }
-}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilderTesting.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilderTesting.java
new file mode 100644
index 00000000000..43f5cbfd5a2
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilderTesting.java
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import java.util.Random;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
+import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+
+public class IssuesChangesNotificationBuilderTesting {
+
+ public static Rule ruleOf(RuleDto rule) {
+ return new Rule(rule.getKey(), rule.getName());
+ }
+
+ public static Rule ruleOf(RuleDefinitionDto rule) {
+ return new Rule(rule.getKey(), rule.getName());
+ }
+
+ public static User userOf(UserDto changeAuthor) {
+ return new User(changeAuthor.getUuid(), changeAuthor.getLogin(), changeAuthor.getName());
+ }
+
+ public static Project projectBranchOf(DbTester db, ComponentDto branch) {
+ BranchDto branchDto = db.getDbClient().branchDao().selectByUuid(db.getSession(), branch.uuid()).get();
+ checkArgument(!branchDto.isMain(), "should be a branch");
+ return new Project.Builder(branch.uuid())
+ .setKey(branch.getKey())
+ .setProjectName(branch.name())
+ .setBranchName(branchDto.getKey())
+ .build();
+ }
+
+ public static Project projectOf(ComponentDto project) {
+ return new Project.Builder(project.uuid())
+ .setKey(project.getKey())
+ .setProjectName(project.name())
+ .build();
+ }
+
+ static ChangedIssue newChangedIssue(String key, Project project, Rule rule) {
+ return new ChangedIssue.Builder(key)
+ .setNewStatus(randomAlphabetic(19))
+ .setProject(project)
+ .setRule(rule)
+ .build();
+ }
+
+ static ChangedIssue newChangedIssue(String key, String status, Project project, String ruleName) {
+ return newChangedIssue(key, status, project, newRule(ruleName));
+ }
+
+ static ChangedIssue newChangedIssue(String key, String status, Project project, Rule rule) {
+ return new ChangedIssue.Builder(key)
+ .setNewStatus(status)
+ .setProject(project)
+ .setRule(rule)
+ .build();
+ }
+
+ static Rule newRule(String ruleName) {
+ return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleName);
+ }
+
+ static Project newProject(String uuid) {
+ return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").build();
+ }
+
+ static Project newBranch(String uuid, String branchName) {
+ return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").setBranchName(branchName).build();
+ }
+
+ static UserChange newUserChange() {
+ return new UserChange(new Random().nextLong(), new User(randomAlphabetic(4), randomAlphabetic(5), randomAlphabetic(6)));
+ }
+
+ static AnalysisChange newAnalysisChange() {
+ return new AnalysisChange(new Random().nextLong());
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationModuleTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationModuleTest.java
new file mode 100644
index 00000000000..2e724dd857a
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationModuleTest.java
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;
+
+public class IssuesChangesNotificationModuleTest {
+ @Test
+ public void verify_count_of_added_components() {
+ ComponentContainer container = new ComponentContainer();
+ new IssuesChangesNotificationModule().configure(container);
+ assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 7);
+ }
+
+
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationTest.java
new file mode 100644
index 00000000000..bbdb2f77446
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/IssuesChangesNotificationTest.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssuesChangesNotificationTest {
+
+ private IssuesChangesNotification notification = new IssuesChangesNotification();
+
+ @Test
+ public void verify_type() {
+ assertThat(notification.getType()).isEqualTo("issues-changes");
+ }
+
+}