import org.sonar.server.exceptions.NotFoundException;
import javax.annotation.CheckForNull;
-
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import com.google.common.collect.ImmutableSet;
import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.resources.Qualifiers;
-import org.sonar.api.utils.Durations;
import org.sonar.core.component.ComponentDto;
import org.sonar.server.computation.ComputationContext;
import org.sonar.server.computation.issue.IssueCache;
import org.sonar.server.computation.issue.RuleCache;
-import org.sonar.server.issue.notification.IssueChangeNotification;
-import org.sonar.server.issue.notification.NewIssuesNotification;
-import org.sonar.server.issue.notification.NewIssuesStatistics;
+import org.sonar.server.issue.notification.*;
import org.sonar.server.notifications.NotificationService;
import org.sonar.server.util.CloseableIterator;
import java.util.Date;
+import java.util.Map;
import java.util.Set;
/**
/**
* Types of the notifications sent by this step
*/
- static final Set<String> NOTIF_TYPES = ImmutableSet.of(IssueChangeNotification.TYPE, NewIssuesNotification.TYPE);
+ static final Set<String> NOTIF_TYPES = ImmutableSet.of(IssueChangeNotification.TYPE, NewIssuesNotification.TYPE, MyNewIssuesNotification.TYPE);
private final IssueCache issueCache;
private final RuleCache rules;
private final NotificationService service;
- private final Durations durations;
+ private NewIssuesNotificationFactory newIssuesNotificationFactory;
- public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, NotificationService service, Durations durations) {
+ public SendIssueNotificationsStep(IssueCache issueCache, RuleCache rules, NotificationService service, NewIssuesNotificationFactory newIssuesNotificationFactory) {
this.issueCache = issueCache;
this.rules = rules;
this.service = service;
- this.durations = durations;
+ this.newIssuesNotificationFactory = newIssuesNotificationFactory;
}
@Override
sendNewIssuesStatistics(context, newIssuesStats);
}
- private void sendNewIssuesStatistics(ComputationContext context, NewIssuesStatistics stats) {
- if (stats.hasIssues()) {
+ private void sendNewIssuesStatistics(ComputationContext context, NewIssuesStatistics statistics) {
+ if (statistics.hasIssues()) {
+ NewIssuesStatistics.Stats globalStatistics = statistics.globalStatistics();
ComponentDto project = context.getProject();
- NewIssuesNotification notification = new NewIssuesNotification();
- notification.setProject(project);
- notification.setAnalysisDate(new Date(context.getReportMetadata().getAnalysisDate()));
- notification.setStatistics(project, stats);
- notification.setDebt(durations.encode(stats.debt()));
+ NewIssuesNotification notification = newIssuesNotificationFactory
+ .newNewIssuesNotication()
+ .setProject(project)
+ .setAnalysisDate(new Date(context.getReportMetadata().getAnalysisDate()))
+ .setStatistics(project, globalStatistics)
+ .setDebt(globalStatistics.debt());
service.deliver(notification);
+
+ // send email to each user having issues
+ for (Map.Entry<String, NewIssuesStatistics.Stats> assigneeAndStatisticsTuple : statistics.assigneesStatistics().entrySet()) {
+ String assignee = assigneeAndStatisticsTuple.getKey();
+ NewIssuesStatistics.Stats assigneeStatistics = assigneeAndStatisticsTuple.getValue();
+ MyNewIssuesNotification myNewIssuesNotification = newIssuesNotificationFactory
+ .newMyNewIssuesNotification()
+ .setAssignee(assignee);
+ myNewIssuesNotification
+ .setProject(project)
+ .setAnalysisDate(new Date(context.getReportMetadata().getAnalysisDate()))
+ .setStatistics(project, assigneeStatistics)
+ .setDebt(assigneeStatistics.debt());
+
+ service.deliver(myNewIssuesNotification);
+ }
}
}
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.Lists;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.plugins.emailnotifications.api.EmailMessage;
+import org.sonar.plugins.emailnotifications.api.EmailTemplate;
+import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Locale;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Base class to create emails for new issues
+ */
+public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate {
+
+ protected static final char NEW_LINE = '\n';
+ protected static final String TAB = " ";
+ protected static final String DOT = ".";
+ protected static final String COUNT = DOT + "count";
+ protected static final String LABEL = DOT + "label";
+
+ static final String FIELD_PROJECT_NAME = "projectName";
+ static final String FIELD_PROJECT_KEY = "projectKey";
+ static final String FIELD_PROJECT_DATE = "projectDate";
+ static final String FIELD_PROJECT_UUID = "projectUuid";
+ static final String FIELD_ASSIGNEE = "assignee";
+
+ protected final EmailSettings settings;
+ protected final I18n i18n;
+
+ public AbstractNewIssuesEmailTemplate(EmailSettings settings, I18n i18n) {
+ this.settings = settings;
+ this.i18n = i18n;
+ }
+
+ public static String encode(String toEncode) {
+ try {
+ return URLEncoder.encode(toEncode, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException("Encoding not supported", e);
+ }
+ }
+
+ @Override
+ public EmailMessage format(Notification notification) {
+ if (shouldNotFormat(notification)) {
+ return null;
+ }
+ String projectName = checkNotNull(notification.getFieldValue(FIELD_PROJECT_NAME));
+
+ StringBuilder message = new StringBuilder();
+ message.append("Project: ").append(projectName).append(NEW_LINE).append(NEW_LINE);
+ appendSeverity(message, notification);
+ appendAssignees(message, notification);
+ appendTags(message, notification);
+ appendComponents(message, notification);
+ appendFooter(message, notification);
+
+ return new EmailMessage()
+ .setMessageId(notification.getType() + "/" + notification.getFieldValue(FIELD_PROJECT_KEY))
+ .setSubject(subject(notification, projectName))
+ .setMessage(message.toString());
+ }
+
+ protected abstract boolean shouldNotFormat(Notification notification);
+
+ protected String subject(Notification notification, String projectName) {
+ return String.format("%s: %s new issues (new debt: %s)",
+ projectName,
+ notification.getFieldValue(METRIC.SEVERITY + COUNT),
+ notification.getFieldValue(METRIC.DEBT + COUNT));
+ }
+
+ private boolean doNotHaveValue(Notification notification, METRIC metric) {
+ return notification.getFieldValue(metric + DOT + "1" + LABEL) == null;
+ }
+
+ private void genericAppendOfMetric(METRIC metric, String label, StringBuilder message, Notification notification) {
+ if (doNotHaveValue(notification, metric)) {
+ return;
+ }
+
+ message
+ .append(TAB)
+ .append(label)
+ .append(NEW_LINE);
+ int i = 1;
+ while (notification.getFieldValue(metric + DOT + i + LABEL) != null && i <= 5) {
+ String name = notification.getFieldValue(metric + DOT + i + LABEL);
+ message
+ .append(TAB).append(TAB)
+ .append(name)
+ .append(": ")
+ .append(notification.getFieldValue(metric + DOT + i + COUNT))
+ .append(NEW_LINE);
+ i += 1;
+ }
+
+ message.append(NEW_LINE);
+ }
+
+ protected void appendAssignees(StringBuilder message, Notification notification) {
+ genericAppendOfMetric(METRIC.ASSIGNEE, "Assignees", message, notification);
+ }
+
+ protected void appendComponents(StringBuilder message, Notification notification) {
+ genericAppendOfMetric(METRIC.COMPONENT, "Most impacted files", message, notification);
+ }
+
+ protected void appendTags(StringBuilder message, Notification notification) {
+ genericAppendOfMetric(METRIC.TAGS, "Tags", message, notification);
+ }
+
+ protected void appendSeverity(StringBuilder message, Notification notification) {
+ message
+ .append(String.format("%s new issues (new debt: %s)",
+ notification.getFieldValue(METRIC.SEVERITY + COUNT),
+ notification.getFieldValue(METRIC.DEBT + COUNT)))
+ .append(NEW_LINE).append(NEW_LINE)
+ .append(TAB)
+ .append("Severity")
+ .append(NEW_LINE)
+ .append(TAB)
+ .append(TAB);
+
+ for (Iterator<String> severityIterator = Lists.reverse(Severity.ALL).iterator(); severityIterator.hasNext();) {
+ String severity = severityIterator.next();
+ String severityLabel = i18n.message(getLocale(), "severity." + severity, severity);
+ message.append(severityLabel).append(": ").append(notification.getFieldValue(METRIC.SEVERITY + DOT + severity + COUNT));
+ if (severityIterator.hasNext()) {
+ message.append(TAB);
+ }
+ }
+
+ message
+ .append(NEW_LINE)
+ .append(NEW_LINE);
+ }
+
+ protected void appendFooter(StringBuilder message, Notification notification) {
+ String projectUuid = notification.getFieldValue(FIELD_PROJECT_UUID);
+ String dateString = notification.getFieldValue(FIELD_PROJECT_DATE);
+ if (projectUuid != null && dateString != null) {
+ Date date = DateUtils.parseDateTime(dateString);
+ String url = String.format("%s/issues/search#projectUuids=%s|createdAt=%s",
+ settings.getServerBaseURL(), encode(projectUuid), encode(DateUtils.formatDateTime(date)));
+ message
+ .append("See it in SonarQube: ")
+ .append(url)
+ .append(NEW_LINE);
+ }
+ }
+
+ private Locale getLocale() {
+ return Locale.ENGLISH;
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.config.EmailSettings;
+import org.sonar.api.i18n.I18n;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC;
+
+import java.util.Date;
+
+/**
+ * Creates email message for notification "my-new-issues".
+ */
+public class MyNewIssuesEmailTemplate extends AbstractNewIssuesEmailTemplate {
+
+ public MyNewIssuesEmailTemplate(EmailSettings settings, I18n i18n) {
+ super(settings, i18n);
+ }
+
+ @Override
+ protected boolean shouldNotFormat(Notification notification) {
+ return !MyNewIssuesNotification.TYPE.equals(notification.getType());
+ }
+
+ @Override
+ protected void appendAssignees(StringBuilder message, Notification notification) {
+ // do nothing as we don't want to print assignees, it's a personalized email for one person
+ }
+
+ @Override
+ protected String subject(Notification notification, String projectName) {
+ return String.format("You have %s new issues on project %s",
+ notification.getFieldValue(METRIC.SEVERITY + COUNT),
+ projectName);
+ }
+
+ @Override
+ protected void appendFooter(StringBuilder message, Notification notification) {
+ String projectUuid = notification.getFieldValue(FIELD_PROJECT_UUID);
+ String dateString = notification.getFieldValue(FIELD_PROJECT_DATE);
+ String assignee = notification.getFieldValue(FIELD_ASSIGNEE);
+ if (projectUuid != null && dateString != null && assignee != null) {
+ Date date = DateUtils.parseDateTime(dateString);
+ String url = String.format("%s/issues/search#projectUuids=%s|assignees=%s|createdAt=%s",
+ settings.getServerBaseURL(),
+ encode(projectUuid),
+ encode(assignee),
+ encode(DateUtils.formatDateTime(date)));
+ message.append("See it in SonarQube: ").append(url).append(NEW_LINE);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.utils.Durations;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.user.index.UserIndex;
+
+import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_ASSIGNEE;
+
+public class MyNewIssuesNotification extends NewIssuesNotification {
+
+ public static final String TYPE = "my-new-issues";
+
+ MyNewIssuesNotification(UserIndex userIndex, DbClient dbClient, Durations durations) {
+ super(TYPE, userIndex, dbClient, durations);
+ }
+
+ public MyNewIssuesNotification setAssignee(String assignee) {
+ setFieldValue(FIELD_ASSIGNEE, assignee);
+
+ return this;
+ }
+}
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.Multimap;
+import org.sonar.api.notifications.*;
+
+import java.util.Collection;
+
+/**
+ * This dispatcher means: "notify me when new issues are introduced during project analysis"
+ */
+public class MyNewIssuesNotificationDispatcher extends NotificationDispatcher {
+
+ public static final String KEY = "MyNewIssues";
+ private final NotificationManager manager;
+
+ public MyNewIssuesNotificationDispatcher(NotificationManager manager) {
+ super(MyNewIssuesNotification.TYPE);
+ this.manager = manager;
+ }
+
+ public static NotificationDispatcherMetadata newMetadata() {
+ return NotificationDispatcherMetadata.create(KEY)
+ .setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
+ .setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
+ }
+
+ @Override
+ public String getKey() {
+ return KEY;
+ }
+
+ @Override
+ public void dispatch(Notification notification, Context context) {
+ String projectKey = notification.getFieldValue("projectKey");
+ String assignee = notification.getFieldValue("assignee");
+ Multimap<String, NotificationChannel> subscribedRecipients = manager.findNotificationSubscribers(this, projectKey);
+
+ Collection<NotificationChannel> channels = subscribedRecipients.get(assignee);
+ for (NotificationChannel channel : channels) {
+ context.addUser(assignee, channel);
+ }
+ }
+
+}
*/
package org.sonar.server.issue.notification;
-import com.google.common.base.Objects;
-import com.google.common.collect.Lists;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.i18n.I18n;
import org.sonar.api.notifications.Notification;
-import org.sonar.api.rule.Severity;
-import org.sonar.api.utils.DateUtils;
-import org.sonar.plugins.emailnotifications.api.EmailMessage;
-import org.sonar.plugins.emailnotifications.api.EmailTemplate;
-import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC;
-import org.sonar.server.user.index.UserDoc;
-import org.sonar.server.user.index.UserIndex;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.Locale;
/**
* Creates email message for notification "new-issues".
*/
-public class NewIssuesEmailTemplate extends EmailTemplate {
-
- public static final String FIELD_PROJECT_NAME = "projectName";
- public static final String FIELD_PROJECT_KEY = "projectKey";
- public static final String FIELD_PROJECT_DATE = "projectDate";
- public static final String FIELD_PROJECT_UUID = "projectUuid";
-
- private static final char NEW_LINE = '\n';
- private static final String TAB = " ";
- private static final String DOT = ".";
- private static final String COUNT = DOT + "count";
- private static final String LABEL = DOT + "label";
-
- private final EmailSettings settings;
- private final I18n i18n;
- private final UserIndex userIndex;
+public class NewIssuesEmailTemplate extends AbstractNewIssuesEmailTemplate {
- public NewIssuesEmailTemplate(EmailSettings settings, I18n i18n, UserIndex userIndex) {
- this.settings = settings;
- this.i18n = i18n;
- this.userIndex = userIndex;
- }
-
- public static String encode(String toEncode) {
- try {
- return URLEncoder.encode(toEncode, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new IllegalStateException("Encoding not supported", e);
- }
+ public NewIssuesEmailTemplate(EmailSettings settings, I18n i18n) {
+ super(settings, i18n);
}
@Override
- public EmailMessage format(Notification notification) {
- if (!NewIssuesNotification.TYPE.equals(notification.getType())) {
- return null;
- }
- String projectName = notification.getFieldValue(FIELD_PROJECT_NAME);
-
- StringBuilder message = new StringBuilder();
- message.append("Project: ").append(projectName).append(NEW_LINE).append(NEW_LINE);
- appendSeverity(message, notification);
- appendAssignees(message, notification);
- appendTags(message, notification);
- appendComponents(message, notification);
- appendFooter(message, notification);
-
- return new EmailMessage()
- .setMessageId("new-issues/" + notification.getFieldValue(FIELD_PROJECT_KEY))
- .setSubject(projectName + ": " + notification.getFieldValue(METRIC.SEVERITY + COUNT) + " new issues")
- .setMessage(message.toString());
- }
-
- private void appendComponents(StringBuilder message, Notification notification) {
- if (notification.getFieldValue(METRIC.COMPONENT + ".1.label") == null) {
- return;
- }
-
- message.append(" Components:\n");
- int i = 1;
- while (notification.getFieldValue(METRIC.COMPONENT + DOT + i + LABEL) != null && i <= 5) {
- String component = notification.getFieldValue(METRIC.COMPONENT + DOT + i + LABEL);
- message
- .append(TAB).append(TAB)
- .append(component)
- .append(" : ")
- .append(notification.getFieldValue(METRIC.COMPONENT + DOT + i + COUNT))
- .append("\n");
- i += 1;
- }
+ protected boolean shouldNotFormat(Notification notification) {
+ return !NewIssuesNotification.TYPE.equals(notification.getType());
}
-
- private void appendAssignees(StringBuilder message, Notification notification) {
- if (notification.getFieldValue(METRIC.LOGIN + DOT + "1" + LABEL) == null) {
- return;
- }
-
- message.append(TAB + "Assignee - ");
- int i = 1;
- while (notification.getFieldValue(METRIC.LOGIN + DOT + i + LABEL) != null && i <= 5) {
- String login = notification.getFieldValue(METRIC.LOGIN + DOT + i + LABEL);
- UserDoc user = userIndex.getNullableByLogin(login);
- String name = user == null ? null : user.name();
- message.append(Objects.firstNonNull(name, login))
- .append(": ")
- .append(notification.getFieldValue(METRIC.LOGIN + DOT + i + COUNT));
- if (i < 5) {
- message.append(TAB);
- }
- i += 1;
- }
-
- message.append("\n");
- }
-
- private void appendTags(StringBuilder message, Notification notification) {
- if (notification.getFieldValue(METRIC.TAGS + DOT + "1" + LABEL) == null) {
- return;
- }
-
- message.append(TAB + "Tags - ");
- int i = 1;
- while (notification.getFieldValue(METRIC.TAGS + DOT + i + LABEL) != null && i <= 5) {
- String tag = notification.getFieldValue(METRIC.TAGS + DOT + i + LABEL);
- message.append(tag)
- .append(": ")
- .append(notification.getFieldValue(METRIC.TAGS + DOT + i + COUNT));
- if (i < 5) {
- message.append(TAB);
- }
- i += 1;
- }
- message.append(NEW_LINE);
- }
-
- private void appendSeverity(StringBuilder message, Notification notification) {
- message.append(notification.getFieldValue(METRIC.SEVERITY + COUNT)).append(" new issues - Total debt: ")
- .append(notification.getFieldValue(METRIC.DEBT + COUNT))
- .append(NEW_LINE).append(NEW_LINE)
- .append(TAB + "Severity - ");
- for (Iterator<String> severityIterator = Lists.reverse(Severity.ALL).iterator(); severityIterator.hasNext();) {
- String severity = severityIterator.next();
- String severityLabel = i18n.message(getLocale(), "severity." + severity, severity);
- message.append(severityLabel).append(": ").append(notification.getFieldValue(METRIC.SEVERITY + DOT + severity + COUNT));
- if (severityIterator.hasNext()) {
- message.append(TAB);
- }
- }
- message.append(NEW_LINE);
- }
-
- private void appendFooter(StringBuilder message, Notification notification) {
- String projectUuid = notification.getFieldValue(FIELD_PROJECT_UUID);
- String dateString = notification.getFieldValue(FIELD_PROJECT_DATE);
- if (projectUuid != null && dateString != null) {
- Date date = DateUtils.parseDateTime(dateString);
- String url = String.format("%s/issues/search#projectUuids=%s|createdAt=%s",
- settings.getServerBaseURL(), encode(projectUuid), encode(DateUtils.formatDateTime(date)));
- message.append("\n").append("See it in SonarQube: ").append(url).append(NEW_LINE);
- }
- }
-
- private Locale getLocale() {
- return Locale.ENGLISH;
- }
-
}
import org.sonar.api.notifications.Notification;
import org.sonar.api.rule.Severity;
import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.Duration;
+import org.sonar.api.utils.Durations;
import org.sonar.core.component.ComponentDto;
+import org.sonar.core.persistence.DbSession;
+import org.sonar.server.db.DbClient;
import org.sonar.server.issue.notification.NewIssuesStatistics.METRIC;
+import org.sonar.server.user.index.UserDoc;
+import org.sonar.server.user.index.UserIndex;
import java.util.Date;
import java.util.List;
public class NewIssuesNotification extends Notification {
+ private static final long serialVersionUID = -6305871981920103093L;
+
public static final String TYPE = "new-issues";
private static final String COUNT = ".count";
+ private static final String LABEL = ".label";
+
+ private final transient UserIndex userIndex;
+ private final transient DbClient dbClient;
+ private final transient Durations durations;
+
+ NewIssuesNotification(UserIndex userIndex, DbClient dbClient, Durations durations) {
+ this(TYPE, userIndex, dbClient, durations);
+ }
- public NewIssuesNotification() {
- super(TYPE);
+ protected NewIssuesNotification(String type, UserIndex userIndex, DbClient dbClient, Durations durations) {
+ super(type);
+ this.userIndex = userIndex;
+ this.dbClient = dbClient;
+ this.durations = durations;
}
public NewIssuesNotification setAnalysisDate(Date d) {
return this;
}
- public NewIssuesNotification setStatistics(Component project, NewIssuesStatistics stats) {
+ public NewIssuesNotification setStatistics(Component project, NewIssuesStatistics.Stats stats) {
setDefaultMessage(stats.countForMetric(SEVERITY) + " new issues on " + project.longName() + ".\n");
setSeverityStatistics(stats);
- setTop5CountsForMetric(stats, METRIC.LOGIN);
- setTop5CountsForMetric(stats, METRIC.TAGS);
- setTop5CountsForMetric(stats, METRIC.COMPONENT);
+ setAssigneesStatistics(stats);
+ setTagsStatistics(stats);
+ setComponentsStatistics(stats);
return this;
}
- public NewIssuesNotification setDebt(String debt) {
- setFieldValue(METRIC.DEBT + COUNT, debt);
- return this;
+ protected void setComponentsStatistics(NewIssuesStatistics.Stats stats) {
+ METRIC metric = METRIC.COMPONENT;
+ List<Multiset.Entry<String>> componentStats = stats.statsForMetric(metric);
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ for (int i = 0; i < 5 && i < componentStats.size(); i++) {
+ String uuid = componentStats.get(i).getElement();
+ String componentName = dbClient.componentDao().getByUuid(dbSession, uuid).name();
+ setFieldValue(metric + "." + (i + 1) + LABEL, componentName);
+ setFieldValue(metric + "." + (i + 1) + COUNT, String.valueOf(componentStats.get(i).getCount()));
+ }
+ }
}
- private void setTop5CountsForMetric(NewIssuesStatistics stats, METRIC metric) {
- List<Multiset.Entry<String>> loginStats = stats.statsForMetric(metric);
- for (int i = 0; i < 5 && i < loginStats.size(); i++) {
- setFieldValue(metric + "." + (i + 1) + COUNT, String.valueOf(loginStats.get(i).getCount()));
- setFieldValue(metric + "." + (i + 1) + ".label", loginStats.get(i).getElement());
+ protected void setTagsStatistics(NewIssuesStatistics.Stats stats) {
+ METRIC metric = METRIC.TAGS;
+ List<Multiset.Entry<String>> metricStats = stats.statsForMetric(metric);
+ for (int i = 0; i < 5 && i < metricStats.size(); i++) {
+ setFieldValue(metric + "." + (i + 1) + COUNT, String.valueOf(metricStats.get(i).getCount()));
+ setFieldValue(metric + "." + (i + 1) + ".label", metricStats.get(i).getElement());
}
}
- private void setSeverityStatistics(NewIssuesStatistics stats) {
+ protected void setAssigneesStatistics(NewIssuesStatistics.Stats stats) {
+ METRIC metric = METRIC.ASSIGNEE;
+ List<Multiset.Entry<String>> metricStats = stats.statsForMetric(metric);
+ for (int i = 0; i < 5 && i < metricStats.size(); i++) {
+ String login = metricStats.get(i).getElement();
+ UserDoc user = userIndex.getNullableByLogin(login);
+ String name = user == null ? login : user.name();
+ setFieldValue(metric + "." + (i + 1) + LABEL, name);
+ setFieldValue(metric + "." + (i + 1) + COUNT, String.valueOf(metricStats.get(i).getCount()));
+ }
+ }
+
+ public NewIssuesNotification setDebt(Duration debt) {
+ setFieldValue(METRIC.DEBT + COUNT, durations.encode(debt));
+ return this;
+ }
+
+ protected void setSeverityStatistics(NewIssuesStatistics.Stats stats) {
setFieldValue(SEVERITY + COUNT, String.valueOf(stats.countForMetric(SEVERITY)));
for (String severity : Severity.ALL) {
setFieldValue(SEVERITY + "." + severity + COUNT, String.valueOf(stats.countForMetric(SEVERITY, severity)));
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.ServerComponent;
+import org.sonar.api.utils.Durations;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.user.index.UserIndex;
+
+public class NewIssuesNotificationFactory implements ServerComponent {
+ private final UserIndex userIndex;
+ private final DbClient dbClient;
+ private final Durations durations;
+
+ public NewIssuesNotificationFactory(UserIndex userIndex, DbClient dbClient, Durations durations) {
+ this.userIndex = userIndex;
+ this.dbClient = dbClient;
+ this.durations = durations;
+ }
+
+ public MyNewIssuesNotification newMyNewIssuesNotification() {
+ return new MyNewIssuesNotification(userIndex, dbClient, durations);
+ }
+
+ public NewIssuesNotification newNewIssuesNotication() {
+ return new NewIssuesNotification(userIndex, dbClient, durations);
+ }
+}
import org.sonar.core.util.MultiSets;
import java.util.EnumMap;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*;
public class NewIssuesStatistics {
- private Map<String, Stats> statisticsByLogin = new HashMap<>();
+ private Map<String, Stats> assigneesStatistics = new LinkedHashMap<>();
private Stats globalStatistics = new Stats();
public void add(Issue issue) {
globalStatistics.add(issue);
String login = issue.assignee();
if (login != null) {
- statisticsForLogin(login).add(issue);
+ getOrCreate(login).add(issue);
}
}
- private Stats statisticsForLogin(String login) {
- if (statisticsByLogin.get(login) == null) {
- statisticsByLogin.put(login, new Stats());
+ private Stats getOrCreate(String assignee) {
+ if (assigneesStatistics.get(assignee) == null) {
+ assigneesStatistics.put(assignee, new Stats());
}
- return statisticsByLogin.get(login);
+ return assigneesStatistics.get(assignee);
}
- public int countForMetric(METRIC metric) {
- return globalStatistics.distributionFor(metric).size();
+ public Map<String, Stats> assigneesStatistics() {
+ return assigneesStatistics;
}
- public int countForMetric(METRIC metric, String label) {
- return globalStatistics.distributionFor(metric).count(label);
- }
-
- public List<Multiset.Entry<String>> statsForMetric(METRIC metric) {
- return MultiSets.listOrderedByHighestCounts(globalStatistics.distributionFor(metric));
- }
-
- public Duration debt() {
- return globalStatistics.debt();
+ public Stats globalStatistics() {
+ return globalStatistics;
}
public boolean hasIssues() {
return globalStatistics.hasIssues();
}
- public enum METRIC {
- SEVERITY(true), TAGS(true), COMPONENT(true), LOGIN(true), DEBT(false);
- private final boolean computeDistribution;
+ enum METRIC {
+ SEVERITY(true), TAGS(true), COMPONENT(true), ASSIGNEE(true), DEBT(false);
+ private final boolean isComputedByDistribution;
- METRIC(boolean computeDistribution) {
- this.computeDistribution = computeDistribution;
+ METRIC(boolean isComputedByDistribution) {
+ this.isComputedByDistribution = isComputedByDistribution;
}
boolean isComputedByDistribution() {
- return this.computeDistribution;
+ return this.isComputedByDistribution;
}
}
- private static class Stats {
+ public static class Stats {
private final Map<METRIC, Multiset<String>> distributions = new EnumMap<>(METRIC.class);
private long debtInMinutes = 0L;
distributions.get(SEVERITY).add(issue.severity());
distributions.get(COMPONENT).add(issue.componentUuid());
if (issue.assignee() != null) {
- distributions.get(LOGIN).add(issue.assignee());
+ distributions.get(ASSIGNEE).add(issue.assignee());
}
for (String tag : issue.tags()) {
distributions.get(TAGS).add(tag);
}
}
- public Multiset<String> distributionFor(METRIC metric) {
- checkArgument(metric.isComputedByDistribution());
- return distributions.get(metric);
+ public int countForMetric(METRIC metric) {
+ return distributionFor(metric).size();
+ }
+
+ public int countForMetric(METRIC metric, String label) {
+ return distributionFor(metric).count(label);
}
public Duration debt() {
public boolean hasIssues() {
return distributions.get(SEVERITY) != null;
}
+
+ public List<Multiset.Entry<String>> statsForMetric(METRIC metric) {
+ return MultiSets.listOrderedByHighestCounts(distributionFor(metric));
+ }
+
+ private Multiset<String> distributionFor(METRIC metric) {
+ checkArgument(metric.isComputedByDistribution());
+ return distributions.get(metric);
+ }
}
}
pico.addSingleton(IssueActionsWriter.class);
pico.addSingleton(IssueQueryService.class);
pico.addSingleton(NewIssuesEmailTemplate.class);
+ pico.addSingleton(MyNewIssuesEmailTemplate.class);
pico.addSingleton(IssueChangesEmailTemplate.class);
pico.addSingleton(ChangesOnMyIssueNotificationDispatcher.class);
pico.addSingleton(ChangesOnMyIssueNotificationDispatcher.newMetadata());
pico.addSingleton(NewIssuesNotificationDispatcher.class);
pico.addSingleton(NewIssuesNotificationDispatcher.newMetadata());
+ pico.addSingleton(MyNewIssuesNotificationDispatcher.class);
+ pico.addSingleton(MyNewIssuesNotificationDispatcher.newMetadata());
pico.addSingleton(DoNotFixNotificationDispatcher.class);
pico.addSingleton(DoNotFixNotificationDispatcher.newMetadata());
+ pico.addSingleton(NewIssuesNotificationFactory.class);
// issue filters
pico.addSingleton(IssueFilterService.class);
import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.notifications.Notification;
import org.sonar.api.rule.Severity;
-import org.sonar.api.utils.Durations;
import org.sonar.api.utils.System2;
import org.sonar.batch.protocol.output.BatchReport;
import org.sonar.server.computation.ComputationContext;
import org.sonar.server.computation.issue.RuleCache;
import org.sonar.server.issue.notification.IssueChangeNotification;
import org.sonar.server.issue.notification.NewIssuesNotification;
+import org.sonar.server.issue.notification.NewIssuesNotificationFactory;
import org.sonar.server.notifications.NotificationService;
import java.io.IOException;
NotificationService notifService = mock(NotificationService.class);
ComputationContext context = mock(ComputationContext.class, Mockito.RETURNS_DEEP_STUBS);
IssueCache issueCache;
- Durations durations = mock(Durations.class);
+ NewIssuesNotificationFactory newIssuesNotificationFactory = mock(NewIssuesNotificationFactory.class, Mockito.RETURNS_DEEP_STUBS);
SendIssueNotificationsStep sut;
@Before
public void setUp() throws Exception {
issueCache = new IssueCache(temp.newFile(), System2.INSTANCE);
- sut = new SendIssueNotificationsStep(issueCache, ruleCache, notifService, durations);
+ sut = new SendIssueNotificationsStep(issueCache, ruleCache, notifService, newIssuesNotificationFactory);
}
@Test
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.Charsets;
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.config.EmailSettings;
+import org.sonar.api.notifications.Notification;
+import org.sonar.core.i18n.DefaultI18n;
+import org.sonar.plugins.emailnotifications.api.EmailMessage;
+import org.sonar.server.user.index.UserDoc;
+import org.sonar.server.user.index.UserIndex;
+
+import java.util.Date;
+import java.util.Locale;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*;
+
+public class MyNewIssuesEmailTemplateTest {
+
+ MyNewIssuesEmailTemplate sut;
+ DefaultI18n i18n;
+ UserIndex userIndex;
+ Date date;
+
+ @Before
+ public void setUp() {
+ EmailSettings settings = mock(EmailSettings.class);
+ when(settings.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org");
+ i18n = mock(DefaultI18n.class);
+ date = new Date();
+ userIndex = mock(UserIndex.class);
+ // returns the login passed in parameter
+ when(userIndex.getNullableByLogin(anyString())).thenAnswer(new Answer<UserDoc>() {
+ @Override
+ public UserDoc answer(InvocationOnMock invocationOnMock) throws Throwable {
+ return new UserDoc().setName((String) invocationOnMock.getArguments()[0]);
+ }
+ });
+ when(i18n.message(any(Locale.class), eq("severity.BLOCKER"), anyString())).thenReturn("Blocker");
+ when(i18n.message(any(Locale.class), eq("severity.CRITICAL"), anyString())).thenReturn("Critical");
+ when(i18n.message(any(Locale.class), eq("severity.MAJOR"), anyString())).thenReturn("Major");
+ when(i18n.message(any(Locale.class), eq("severity.MINOR"), anyString())).thenReturn("Minor");
+ when(i18n.message(any(Locale.class), eq("severity.INFO"), anyString())).thenReturn("Info");
+
+ sut = new MyNewIssuesEmailTemplate(settings, i18n);
+ }
+
+ @Test
+ public void no_format_if_not_the_correct_notif() {
+ Notification notification = new Notification("new-issues");
+ EmailMessage message = sut.format(notification);
+ assertThat(message).isNull();
+ }
+
+ @Test
+ public void format_email_with_all_fields_filled() throws Exception {
+ Notification notification = newNotification();
+ addTags(notification);
+ addComponents(notification);
+
+ EmailMessage message = sut.format(notification);
+
+ // TODO datetime to be completed when test is isolated from JVM timezone
+ String file = IOUtils.toString(getClass().getResource("MyNewIssuesEmailTemplateTest/email_with_all_details.txt"), Charsets.UTF_8);
+ assertThat(message.getMessage()).startsWith(file);
+ }
+
+ @Test
+ public void message_id() throws Exception {
+ Notification notification = newNotification();
+
+ EmailMessage message = sut.format(notification);
+
+ assertThat(message.getMessageId()).isEqualTo("my-new-issues/org.apache:struts");
+ }
+
+ @Test
+ public void subject() throws Exception {
+ Notification notification = newNotification();
+
+ EmailMessage message = sut.format(notification);
+
+ assertThat(message.getSubject()).isEqualTo("You have 32 new issues on project Struts");
+ }
+
+ @Test
+ public void format_email_with_no_assignees_tags_nor_components() throws Exception {
+ Notification notification = newNotification();
+
+ EmailMessage message = sut.format(notification);
+
+ // TODO datetime to be completed when test is isolated from JVM timezone
+ String file = IOUtils.toString(getClass().getResource("MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt"), Charsets.UTF_8);
+ assertThat(message.getMessage()).startsWith(file);
+ }
+
+ @Test
+ public void do_not_add_footer_when_properties_missing() {
+ Notification notification = new Notification(MyNewIssuesNotification.TYPE)
+ .setFieldValue(SEVERITY + ".count", "32")
+ .setFieldValue("projectName", "Struts");
+
+ EmailMessage message = sut.format(notification);
+ assertThat(message.getMessage()).doesNotContain("See it");
+ }
+
+ private Notification newNotification() {
+ return new Notification(MyNewIssuesNotification.TYPE)
+ .setFieldValue("projectName", "Struts")
+ .setFieldValue("projectKey", "org.apache:struts")
+ .setFieldValue("projectUuid", "ABCDE")
+ .setFieldValue("projectDate", "2010-05-18T14:50:45+0000")
+ .setFieldValue("assignee", "lo.gin")
+ .setFieldValue(DEBT + ".count", "1d3h")
+ .setFieldValue(SEVERITY + ".count", "32")
+ .setFieldValue(SEVERITY + ".INFO.count", "1")
+ .setFieldValue(SEVERITY + ".MINOR.count", "3")
+ .setFieldValue(SEVERITY + ".MAJOR.count", "10")
+ .setFieldValue(SEVERITY + ".CRITICAL.count", "5")
+ .setFieldValue(SEVERITY + ".BLOCKER.count", "0");
+ }
+
+ private void addTags(Notification notification) {
+ notification
+ .setFieldValue(TAGS + ".1.label", "oscar")
+ .setFieldValue(TAGS + ".1.count", "3")
+ .setFieldValue(TAGS + ".2.label", "cesar")
+ .setFieldValue(TAGS + ".2.count", "10");
+ }
+
+ private void addComponents(Notification notification) {
+ notification
+ .setFieldValue(COMPONENT + ".1.label", "/path/to/file")
+ .setFieldValue(COMPONENT + ".1.count", "3")
+ .setFieldValue(COMPONENT + ".2.label", "/path/to/directory")
+ .setFieldValue(COMPONENT + ".2.count", "7");
+ }
+}
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.HashMultimap;
+import com.google.common.collect.Multimap;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.notifications.Notification;
+import org.sonar.api.notifications.NotificationChannel;
+import org.sonar.api.notifications.NotificationDispatcher;
+import org.sonar.api.notifications.NotificationManager;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+
+public class MyNewIssuesNotificationDispatcherTest {
+
+ private MyNewIssuesNotificationDispatcher sut;
+
+ private NotificationManager notificationManager = mock(NotificationManager.class);
+ private NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class);
+ private NotificationChannel emailChannel = mock(NotificationChannel.class);
+ private NotificationChannel twitterChannel = mock(NotificationChannel.class);
+
+
+ @Before
+ public void setUp() {
+ sut = new MyNewIssuesNotificationDispatcher(notificationManager);
+ }
+
+ @Test
+ public void do_not_dispatch_if_no_new_notification() throws Exception {
+ Notification notification = new Notification("other-notif");
+ sut.performDispatch(notification, context);
+
+ verify(context, never()).addUser(any(String.class), any(NotificationChannel.class));
+ }
+
+ @Test
+ public void dispatch_to_users_who_have_subscribed_to_notification_and_project() {
+ Multimap<String, NotificationChannel> recipients = HashMultimap.create();
+ recipients.put("user1", emailChannel);
+ recipients.put("user2", twitterChannel);
+ when(notificationManager.findNotificationSubscribers(sut, "struts")).thenReturn(recipients);
+
+ Notification notification = new Notification(MyNewIssuesNotification.TYPE)
+ .setFieldValue("projectKey", "struts")
+ .setFieldValue("assignee", "user1");
+ sut.performDispatch(notification, context);
+
+ verify(context).addUser("user1", emailChannel);
+ verifyNoMoreInteractions(context);
+ }
+}
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.api.utils.Durations;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.user.index.UserIndex;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_ASSIGNEE;
+
+public class MyNewIssuesNotificationTest {
+
+ MyNewIssuesNotification sut = new MyNewIssuesNotification(mock(UserIndex.class), mock(DbClient.class), mock(Durations.class));
+
+ @Test
+ public void set_assignee() throws Exception {
+ sut.setAssignee("myAssignee");
+
+ assertThat(sut.getFieldValue(FIELD_ASSIGNEE)).isEqualTo("myAssignee");
+ }
+
+ @Test
+ public void set_with_a_specific_type() throws Exception {
+ assertThat(sut.getType()).isEqualTo(MyNewIssuesNotification.TYPE);
+
+ }
+}
*/
package org.sonar.server.issue.notification;
+import com.google.common.base.Charsets;
+import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
public class NewIssuesEmailTemplateTest {
- private static final String EMAIL_HEADER = "Project: Struts\n\n";
- private static final String EMAIL_TOTAL_ISSUES = "32 new issues - Total debt: 1d3h\n\n";
- private static final String EMAIL_ISSUES = " Severity - Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1\n";
- private static final String EMAIL_ASSIGNEES = " Assignee - robin.williams: 5 al.pacino: 7 \n";
- private static final String EMAIL_TAGS = " Tags - oscar: 3 cesar: 10 \n";
- private static final String EMAIL_COMPONENTS = " Components:\n" +
- " /path/to/file : 3\n" +
- " /path/to/directory : 7\n";
- private static final String EMAIL_FOOTER = "\nSee it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|createdAt=2010-05-1";
-
NewIssuesEmailTemplate template;
DefaultI18n i18n;
UserIndex userIndex;
when(i18n.message(any(Locale.class), eq("severity.MINOR"), anyString())).thenReturn("Minor");
when(i18n.message(any(Locale.class), eq("severity.INFO"), anyString())).thenReturn("Info");
- template = new NewIssuesEmailTemplate(settings, i18n, userIndex);
+ template = new NewIssuesEmailTemplate(settings, i18n);
}
@Test
- public void shouldNotFormatIfNotCorrectNotification() {
- Notification notification = new Notification("other-notif");
+ public void no_format_is_not_the_correct_notification() {
+ Notification notification = new Notification("my-new-issues");
EmailMessage message = template.format(notification);
assertThat(message).isNull();
}
- /**
- * <pre>
- * Subject: Project Struts, new issues
- * From: Sonar
- *
- * Project: Foo
- * 32 new issues - Total debt: 1d3h
- *
- * Severity - Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1
- * Assignee - robin.williams: 5 al.pacino: 7
- * Tags - oscar: 3 cesar:10
- * Components:
- * /path/to/file : 3
- * /path/to/directoy : 7
- *
- * See it in SonarQube: http://nemo.sonarsource.org/drilldown/measures/org.sonar.foo:foo?metric=new_violations
- * </pre>
- */
@Test
- public void format_email_with_all_fields_filled() {
+ public void message_id() throws Exception {
+ Notification notification = newNotification();
+
+ EmailMessage message = template.format(notification);
+
+ assertThat(message.getMessageId()).isEqualTo("new-issues/org.apache:struts");
+ }
+
+ @Test
+ public void subject() throws Exception {
+ Notification notification = newNotification();
+
+ EmailMessage message = template.format(notification);
+
+ assertThat(message.getSubject()).isEqualTo("Struts: 32 new issues (new debt: 1d3h)");
+ }
+
+ @Test
+ public void format_email_with_all_fields_filled() throws Exception {
Notification notification = newNotification();
addAssignees(notification);
addTags(notification);
EmailMessage message = template.format(notification);
- assertThat(message.getMessageId()).isEqualTo("new-issues/org.apache:struts");
- assertThat(message.getSubject()).isEqualTo("Struts: 32 new issues");
-
// TODO datetime to be completed when test is isolated from JVM timezone
- assertThat(message.getMessage()).startsWith("" +
- EMAIL_HEADER +
- EMAIL_TOTAL_ISSUES +
- EMAIL_ISSUES +
- EMAIL_ASSIGNEES +
- EMAIL_TAGS +
- EMAIL_COMPONENTS +
- EMAIL_FOOTER);
+ String expectedContent = IOUtils.toString(getClass().getResource("NewIssuesEmailTemplateTest/email_with_all_details.txt"), Charsets.UTF_8);
+ assertThat(message.getMessage()).startsWith(expectedContent);
}
@Test
public void format_email_with_no_assignees_tags_nor_components() throws Exception {
Notification notification = newNotification();
- when(i18n.message(any(Locale.class), eq("severity.BLOCKER"), anyString())).thenReturn("Blocker");
- when(i18n.message(any(Locale.class), eq("severity.CRITICAL"), anyString())).thenReturn("Critical");
- when(i18n.message(any(Locale.class), eq("severity.MAJOR"), anyString())).thenReturn("Major");
- when(i18n.message(any(Locale.class), eq("severity.MINOR"), anyString())).thenReturn("Minor");
- when(i18n.message(any(Locale.class), eq("severity.INFO"), anyString())).thenReturn("Info");
-
EmailMessage message = template.format(notification);
- assertThat(message.getMessageId()).isEqualTo("new-issues/org.apache:struts");
- assertThat(message.getSubject()).isEqualTo("Struts: 32 new issues");
-
// TODO datetime to be completed when test is isolated from JVM timezone
- assertThat(message.getMessage()).startsWith("" +
- EMAIL_HEADER +
- EMAIL_TOTAL_ISSUES +
- EMAIL_ISSUES +
- EMAIL_FOOTER);
+ String expectedContent = IOUtils.toString(getClass().getResource("NewIssuesEmailTemplateTest/email_with_partial_details.txt"), Charsets.UTF_8);
+ assertThat(message.getMessage()).startsWith(expectedContent);
}
@Test
public void do_not_add_footer_when_properties_missing() {
- Notification notification = new NewIssuesNotification()
+ Notification notification = new Notification(NewIssuesNotification.TYPE)
.setFieldValue(SEVERITY + ".count", "32")
.setFieldValue("projectName", "Struts");
EmailMessage message = template.format(notification);
+
assertThat(message.getMessage()).doesNotContain("See it");
}
private Notification newNotification() {
- return new NewIssuesNotification()
+ return new Notification(NewIssuesNotification.TYPE)
.setFieldValue("projectName", "Struts")
.setFieldValue("projectKey", "org.apache:struts")
.setFieldValue("projectUuid", "ABCDE")
private void addAssignees(Notification notification) {
notification
- .setFieldValue(LOGIN + ".1.label", "robin.williams")
- .setFieldValue(LOGIN + ".1.count", "5")
- .setFieldValue(LOGIN + ".2.label", "al.pacino")
- .setFieldValue(LOGIN + ".2.count", "7");
+ .setFieldValue(ASSIGNEE + ".1.label", "robin.williams")
+ .setFieldValue(ASSIGNEE + ".1.count", "5")
+ .setFieldValue(ASSIGNEE + ".2.label", "al.pacino")
+ .setFieldValue(ASSIGNEE + ".2.count", "7");
}
private void addTags(Notification notification) {
import com.google.common.collect.Multimap;
import org.junit.Before;
import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.notifications.NotificationDispatcher;
public class NewIssuesNotificationDispatcherTest {
- @Mock
- private NotificationManager notifications;
-
- @Mock
- private NotificationDispatcher.Context context;
-
- @Mock
- private NotificationChannel emailChannel;
-
- @Mock
- private NotificationChannel twitterChannel;
-
- private NewIssuesNotificationDispatcher dispatcher;
+ private NotificationManager notifications = mock(NotificationManager.class);
+ private NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class);
+ private NotificationChannel emailChannel = mock(NotificationChannel.class);
+ private NotificationChannel twitterChannel = mock(NotificationChannel.class);
+ private NewIssuesNotificationDispatcher dispatcher = mock(NewIssuesNotificationDispatcher.class);
@Before
- public void init() {
- MockitoAnnotations.initMocks(this);
+ public void setUp() {
dispatcher = new NewIssuesNotificationDispatcher(notifications);
}
recipients.put("user2", twitterChannel);
when(notifications.findNotificationSubscribers(dispatcher, "struts")).thenReturn(recipients);
- Notification notification = new NewIssuesNotification().setFieldValue("projectKey", "struts");
+ Notification notification = new Notification(NewIssuesNotification.TYPE).setFieldValue("projectKey", "struts");
dispatcher.performDispatch(notification, context);
verify(context).addUser("user1", emailChannel);
import com.google.common.collect.Lists;
import org.junit.Test;
+import org.mockito.Mockito;
import org.sonar.api.issue.internal.DefaultIssue;
import org.sonar.api.rule.Severity;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.Duration;
+import org.sonar.api.utils.Durations;
import org.sonar.core.component.ComponentDto;
+import org.sonar.core.persistence.DbSession;
import org.sonar.server.component.ComponentTesting;
+import org.sonar.server.db.DbClient;
+import org.sonar.server.user.index.UserIndex;
import java.util.Date;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*;
public class NewIssuesNotificationTest {
- NewIssuesNotification sut = new NewIssuesNotification();
- NewIssuesStatistics stats = new NewIssuesStatistics();
+ NewIssuesStatistics.Stats stats = new NewIssuesStatistics.Stats();
+ UserIndex userIndex = mock(UserIndex.class);
+ DbClient dbClient = mock(DbClient.class, Mockito.RETURNS_DEEP_STUBS);
+ Durations durations = mock(Durations.class);
+ NewIssuesNotification sut = new NewIssuesNotification(userIndex, dbClient, durations);
@Test
public void set_project() throws Exception {
.setLongName("project-long-name");
addIssueNTimes(newIssue1(), 5);
addIssueNTimes(newIssue2(), 3);
+ when(dbClient.componentDao().getByUuid(any(DbSession.class), eq("file-uuid")).name()).thenReturn("file-name");
+ when(dbClient.componentDao().getByUuid(any(DbSession.class), eq("directory-uuid")).name()).thenReturn("directory-name");
sut.setStatistics(component, stats);
assertThat(sut.getFieldValue(SEVERITY + ".INFO.count")).isEqualTo("5");
assertThat(sut.getFieldValue(SEVERITY + ".BLOCKER.count")).isEqualTo("3");
- assertThat(sut.getFieldValue(LOGIN + ".1.label")).isEqualTo("maynard");
- assertThat(sut.getFieldValue(LOGIN + ".1.count")).isEqualTo("5");
- assertThat(sut.getFieldValue(LOGIN + ".2.label")).isEqualTo("keenan");
- assertThat(sut.getFieldValue(LOGIN + ".2.count")).isEqualTo("3");
+ assertThat(sut.getFieldValue(ASSIGNEE + ".1.label")).isEqualTo("maynard");
+ assertThat(sut.getFieldValue(ASSIGNEE + ".1.count")).isEqualTo("5");
+ assertThat(sut.getFieldValue(ASSIGNEE + ".2.label")).isEqualTo("keenan");
+ assertThat(sut.getFieldValue(ASSIGNEE + ".2.count")).isEqualTo("3");
assertThat(sut.getFieldValue(TAGS + ".1.label")).isEqualTo("owasp");
assertThat(sut.getFieldValue(TAGS + ".1.count")).isEqualTo("8");
assertThat(sut.getFieldValue(TAGS + ".2.label")).isEqualTo("bug");
assertThat(sut.getFieldValue(TAGS + ".2.count")).isEqualTo("5");
- assertThat(sut.getFieldValue(COMPONENT + ".1.label")).isEqualTo("file-uuid");
+ assertThat(sut.getFieldValue(COMPONENT + ".1.label")).isEqualTo("file-name");
assertThat(sut.getFieldValue(COMPONENT + ".1.count")).isEqualTo("5");
- assertThat(sut.getFieldValue(COMPONENT + ".2.label")).isEqualTo("directory-uuid");
+ assertThat(sut.getFieldValue(COMPONENT + ".2.label")).isEqualTo("directory-name");
assertThat(sut.getFieldValue(COMPONENT + ".2.count")).isEqualTo("3");
assertThat(sut.getDefaultMessage()).startsWith("8 new issues on project-long-name");
}
@Test
public void set_debt() throws Exception {
- sut.setDebt("55min");
+ when(durations.encode(any(Duration.class))).thenReturn("55min");
+
+ sut.setDebt(Duration.create(55));
assertThat(sut.getFieldValue(DEBT + ".count")).isEqualTo("55min");
}
sut.add(issue.setAssignee("james"));
sut.add(issue.setAssignee("keenan"));
- assertThat(countDistribution(METRIC.LOGIN, "maynard")).isEqualTo(1);
- assertThat(countDistribution(METRIC.LOGIN, "james")).isEqualTo(1);
- assertThat(countDistribution(METRIC.LOGIN, "keenan")).isEqualTo(1);
- assertThat(countDistribution(METRIC.LOGIN, "wrong.login")).isEqualTo(0);
+ assertThat(countDistribution(METRIC.ASSIGNEE, "maynard")).isEqualTo(1);
+ assertThat(countDistribution(METRIC.ASSIGNEE, "james")).isEqualTo(1);
+ assertThat(countDistribution(METRIC.ASSIGNEE, "keenan")).isEqualTo(1);
+ assertThat(countDistribution(METRIC.ASSIGNEE, "wrong.login")).isEqualTo(0);
assertThat(countDistribution(METRIC.COMPONENT, "file-uuid")).isEqualTo(3);
assertThat(countDistribution(METRIC.COMPONENT, "wrong-uuid")).isEqualTo(0);
assertThat(countDistribution(METRIC.SEVERITY, Severity.INFO)).isEqualTo(3);
assertThat(countDistribution(METRIC.SEVERITY, Severity.CRITICAL)).isEqualTo(0);
assertThat(countDistribution(METRIC.TAGS, "owasp")).isEqualTo(3);
assertThat(countDistribution(METRIC.TAGS, "wrong-tag")).isEqualTo(0);
- assertThat(sut.debt().toMinutes()).isEqualTo(15L);
+ assertThat(sut.globalStatistics().debt().toMinutes()).isEqualTo(15L);
}
private int countDistribution(METRIC metric, String label) {
- return sut.countForMetric(metric, label);
+ return sut.globalStatistics().countForMetric(metric, label);
}
private DefaultIssue defaultIssue() {
--- /dev/null
+Project: Struts
+
+32 new issues (new debt: 1d3h)
+
+ Severity
+ Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1
+
+ Tags
+ oscar: 3
+ cesar: 10
+
+ Most impacted files
+ /path/to/file: 3
+ /path/to/directory: 7
+
+See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1
\ No newline at end of file
--- /dev/null
+Project: Struts
+
+32 new issues (new debt: 1d3h)
+
+ Severity
+ Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1
+
+See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|assignees=lo.gin|createdAt=2010-05-1
\ No newline at end of file
--- /dev/null
+Project: Struts
+
+32 new issues (new debt: 1d3h)
+
+ Severity
+ Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1
+
+ Assignees
+ robin.williams: 5
+ al.pacino: 7
+
+ Tags
+ oscar: 3
+ cesar: 10
+
+ Most impacted files
+ /path/to/file: 3
+ /path/to/directory: 7
+
+See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|createdAt=2010-05-1
\ No newline at end of file
--- /dev/null
+Project: Struts
+
+32 new issues (new debt: 1d3h)
+
+ Severity
+ Blocker: 0 Critical: 5 Major: 10 Minor: 3 Info: 1
+
+See it in SonarQube: http://nemo.sonarsource.org/issues/search#projectUuids=ABCDE|createdAt=2010-05-1
\ No newline at end of file
notification.dispatcher.NewIssues=New issues
notification.dispatcher.NewAlerts=New quality gate status
notification.dispatcher.NewFalsePositiveIssue=Issues resolved as false positive or won't fix
+notification.dispatcher.MyNewIssues=My New Issues
#------------------------------------------------------------------------------