Sfoglia il codice sorgente

notify users when they are automatically assigned to issues - SONAR-6106 SONAR-6045

tags/5.2-RC1
Teryk Bellahsene 9 anni fa
parent
commit
4411bd57b2
24 ha cambiato i file con 962 aggiunte e 315 eliminazioni
  1. 0
    1
      server/sonar-server/src/main/java/org/sonar/server/component/db/ComponentDao.java
  2. 31
    15
      server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java
  3. 188
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java
  4. 72
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java
  5. 42
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotification.java
  6. 64
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcher.java
  7. 5
    155
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java
  8. 61
    15
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java
  9. 46
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationFactory.java
  10. 34
    30
      server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesStatistics.java
  11. 4
    0
      server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
  12. 3
    3
      server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java
  13. 165
    0
      server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest.java
  14. 73
    0
      server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcherTest.java
  15. 48
    0
      server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationTest.java
  16. 35
    63
      server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java
  17. 7
    18
      server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java
  18. 25
    9
      server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java
  19. 6
    6
      server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesStatisticsTest.java
  20. 16
    0
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_all_details.txt
  21. 8
    0
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt
  22. 20
    0
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_all_details.txt
  23. 8
    0
      server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_partial_details.txt
  24. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 0
- 1
server/sonar-server/src/main/java/org/sonar/server/component/db/ComponentDao.java Vedi File

@@ -36,7 +36,6 @@ import org.sonar.server.db.BaseDao;
import org.sonar.server.exceptions.NotFoundException;

import javax.annotation.CheckForNull;

import java.util.Collection;
import java.util.Collections;
import java.util.List;

+ 31
- 15
server/sonar-server/src/main/java/org/sonar/server/computation/step/SendIssueNotificationsStep.java Vedi File

@@ -22,18 +22,16 @@ package org.sonar.server.computation.step;
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;

/**
@@ -45,18 +43,18 @@ public class SendIssueNotificationsStep implements ComputationStep {
/**
* 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
@@ -94,15 +92,33 @@ public class SendIssueNotificationsStep implements ComputationStep {
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);
}
}
}


+ 188
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java Vedi File

@@ -0,0 +1,188 @@
/*
* 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;
}

}

+ 72
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java Vedi File

@@ -0,0 +1,72 @@
/*
* 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);
}
}
}

+ 42
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotification.java Vedi File

@@ -0,0 +1,42 @@
/*
* 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;
}
}

+ 64
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcher.java Vedi File

@@ -0,0 +1,64 @@
/*
* 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);
}
}

}

+ 5
- 155
server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesEmailTemplate.java Vedi File

@@ -19,171 +19,21 @@
*/
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;
}

}

+ 61
- 15
server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java Vedi File

@@ -24,8 +24,14 @@ import org.sonar.api.component.Component;
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;
@@ -35,11 +41,25 @@ import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.SEV

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) {
@@ -54,31 +74,57 @@ public class NewIssuesNotification extends Notification {
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)));

+ 46
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotificationFactory.java Vedi File

@@ -0,0 +1,46 @@
/*
* 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);
}
}

+ 34
- 30
server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesStatistics.java Vedi File

@@ -27,7 +27,7 @@ import org.sonar.api.utils.Duration;
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;

@@ -35,58 +35,50 @@ import static com.google.common.base.Preconditions.checkArgument;
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;

@@ -102,7 +94,7 @@ public class NewIssuesStatistics {
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);
@@ -113,9 +105,12 @@ public class NewIssuesStatistics {
}
}

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() {
@@ -125,5 +120,14 @@ public class NewIssuesStatistics {
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);
}
}
}

+ 4
- 0
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java Vedi File

@@ -538,13 +538,17 @@ class ServerComponents {
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);

+ 3
- 3
server/sonar-server/src/test/java/org/sonar/server/computation/step/SendIssueNotificationsStepTest.java Vedi File

@@ -27,7 +27,6 @@ import org.mockito.Mockito;
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;
@@ -35,6 +34,7 @@ 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.NewIssuesNotificationFactory;
import org.sonar.server.notifications.NotificationService;

import java.io.IOException;
@@ -51,13 +51,13 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
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

+ 165
- 0
server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest.java Vedi File

@@ -0,0 +1,165 @@
/*
* 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");
}
}

+ 73
- 0
server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcherTest.java Vedi File

@@ -0,0 +1,73 @@
/*
* 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);
}
}

+ 48
- 0
server/sonar-server/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationTest.java Vedi File

@@ -0,0 +1,48 @@
/*
* 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);

}
}

+ 35
- 63
server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest.java Vedi File

@@ -19,6 +19,8 @@
*/
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;
@@ -40,16 +42,6 @@ import static org.sonar.server.issue.notification.NewIssuesStatistics.METRIC.*;

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;
@@ -73,36 +65,36 @@ public class NewIssuesEmailTemplateTest {
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);
@@ -110,55 +102,35 @@ public class NewIssuesEmailTemplateTest {

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")
@@ -174,10 +146,10 @@ public class NewIssuesEmailTemplateTest {

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) {

+ 7
- 18
server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationDispatcherTest.java Vedi File

@@ -23,8 +23,6 @@ import com.google.common.collect.HashMultimap;
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;
@@ -35,23 +33,14 @@ import static org.mockito.Mockito.*;

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);
}

@@ -70,7 +59,7 @@ public class NewIssuesNotificationDispatcherTest {
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);

+ 25
- 9
server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java Vedi File

@@ -22,22 +22,34 @@ package org.sonar.server.issue.notification;

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 {
@@ -68,29 +80,33 @@ public class NewIssuesNotificationTest {
.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");
}

+ 6
- 6
server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesStatisticsTest.java Vedi File

@@ -41,21 +41,21 @@ public class NewIssuesStatisticsTest {
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() {

+ 16
- 0
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_all_details.txt Vedi File

@@ -0,0 +1,16 @@
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

+ 8
- 0
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/MyNewIssuesEmailTemplateTest/email_with_no_assignee_tags_components.txt Vedi File

@@ -0,0 +1,8 @@
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

+ 20
- 0
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_all_details.txt Vedi File

@@ -0,0 +1,20 @@
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

+ 8
- 0
server/sonar-server/src/test/resources/org/sonar/server/issue/notification/NewIssuesEmailTemplateTest/email_with_partial_details.txt Vedi File

@@ -0,0 +1,8 @@
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

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Vedi File

@@ -2040,6 +2040,7 @@ notification.dispatcher.ChangesOnMyIssue=Changes in issues assigned to me or rep
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


#------------------------------------------------------------------------------

Loading…
Annulla
Salva