Browse Source

SONAR-12745 Adjust `changes on my issue` email notification for Security Hotspots

tags/8.2.0.32929
Jacek 4 years ago
parent
commit
ec1f6fbe74
18 changed files with 558 additions and 145 deletions
  1. 1
    1
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/NotificationFactory.java
  2. 4
    3
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java
  3. 13
    15
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplate.java
  4. 70
    5
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java
  5. 16
    3
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java
  6. 9
    3
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java
  7. 65
    0
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/RuleGroup.java
  8. 4
    4
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandlerTest.java
  9. 214
    75
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplateTest.java
  10. 5
    2
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotificationTest.java
  11. 2
    3
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandlerTest.java
  12. 6
    5
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationTest.java
  13. 116
    15
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplateTest.java
  14. 28
    6
      server/sonar-server-common/src/testFixtures/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilderTesting.java
  15. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
  16. 2
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
  17. 1
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  18. 1
    2
      sonar-plugin-api/src/main/java/org/sonar/api/rules/RuleType.java

+ 1
- 1
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/NotificationFactory.java View File

@@ -111,7 +111,7 @@ public class NotificationFactory {

private IssuesChangesNotificationBuilder.Rule getRuleByRuleKey(RuleKey ruleKey) {
return ruleRepository.findByKey(ruleKey)
.map(t -> new IssuesChangesNotificationBuilder.Rule(ruleKey, t.getName()))
.map(t -> new IssuesChangesNotificationBuilder.Rule(ruleKey, t.getType(), t.getName()))
.orElseThrow(() -> new IllegalStateException("Can not find rule " + ruleKey + " in RuleRepository"));
}


+ 4
- 3
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java View File

@@ -92,6 +92,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.ce.task.projectanalysis.component.Component.Type;
import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
import static org.sonar.ce.task.projectanalysis.step.SendIssueNotificationsStep.NOTIF_TYPES;
@@ -133,7 +134,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
public DbTester db = DbTester.create(System2.INSTANCE);

private final Random random = new Random();
private final RuleType[] RULE_TYPES_EXCEPT_HOTSPOTS = Stream.of(RuleType.values()).filter(r -> r != RuleType.SECURITY_HOTSPOT).toArray(RuleType[]::new);
private final RuleType[] RULE_TYPES_EXCEPT_HOTSPOTS = Stream.of(RuleType.values()).filter(r -> r != SECURITY_HOTSPOT).toArray(RuleType[]::new);
private final RuleType randomRuleType = RULE_TYPES_EXCEPT_HOTSPOTS[random.nextInt(RULE_TYPES_EXCEPT_HOTSPOTS.length)];
@SuppressWarnings("unchecked")
private Class<Map<String, UserDto>> assigneeCacheType = (Class<Map<String, UserDto>>) (Object) Map.class;
@@ -460,7 +461,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
.isEmpty();
Tuple[] expected = stream(users).map(user -> tuple(user.getUuid(), user.getUuid(), user.getId(), user.getLogin())).toArray(Tuple[]::new);
assertThat(cache.entrySet())
.extracting(t -> t.getKey(), t -> t.getValue().getUuid(), t -> t.getValue().getId(), t -> t.getValue().getLogin())
.extracting(Map.Entry::getKey, t -> t.getValue().getUuid(), t -> t.getValue().getId(), t -> t.getValue().getLogin())
.containsOnly(expected);
}

@@ -488,7 +489,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
}

@Test
public void dont_send_issues_change_notification_for_hotspot() {
public void do_not_send_new_issues_notifications_for_hotspot() {
UserDto user = db.users().insertUser();
ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName());
ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName());

+ 13
- 15
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplate.java View File

@@ -21,7 +21,6 @@ package org.sonar.server.issue.notification;

import com.google.common.collect.ListMultimap;
import com.google.common.collect.SetMultimap;
import java.util.Collection;
import java.util.List;
import javax.annotation.CheckForNull;
import org.sonar.api.config.EmailSettings;
@@ -34,9 +33,13 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;

import static com.google.common.base.Preconditions.checkState;
import static java.util.function.Function.identity;
import static org.sonar.api.issue.Issue.STATUS_CLOSED;
import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.core.util.stream.MoreCollectors.index;
import static org.sonar.server.issue.notification.RuleGroup.ISSUES;
import static org.sonar.server.issue.notification.RuleGroup.SECURITY_HOTSPOTS;
import static org.sonar.server.issue.notification.RuleGroup.resolveGroup;

/**
* Creates email message for notification "Changes on my issues".
@@ -109,33 +112,28 @@ public class ChangesOnMyIssuesEmailTemplate extends IssueChangesEmailTemplate {
return new EmailMessage()
.setFrom(user.getName().orElse(user.getLogin()))
.setMessageId("changes-on-my-issues")
.setSubject("A manual update has changed some of your issues")
.setSubject("A manual update has changed some of your issues/hotspots")
.setHtmlMessage(buildMultiProjectMessage(notification));
}

private String buildMultiProjectMessage(ChangesOnMyIssuesNotification notification) {
ListMultimap<RuleGroup, ChangedIssue> issuesAndHotspots = notification.getChangedIssues().values().stream()
.collect(index(changedIssue -> resolveGroup(changedIssue.getRule().getRuleType()), identity()));

List<ChangedIssue> issues = issuesAndHotspots.get(ISSUES);
List<ChangedIssue> hotspots = issuesAndHotspots.get(SECURITY_HOTSPOTS);

StringBuilder sb = new StringBuilder();
paragraph(sb, s -> s.append("Hi,"));
paragraph(sb, s -> {
SetMultimap<Project, ChangedIssue> changedIssues = notification.getChangedIssues();
s.append("A manual change has updated ").append(issuesOrAnIssue(changedIssues))
.append(" assigned to you:");
});
paragraph(sb, s -> s.append("A manual change has updated ").append(RuleGroup.formatIssuesOrHotspots(issues, hotspots)).append(" assigned to you:"));

addIssuesByProjectThenRule(sb, notification.getChangedIssues());
addIssuesAndHotspotsByProjectThenRule(sb, notification.getChangedIssues());

addFooter(sb, NOTIFICATION_NAME_I18N_KEY);

return sb.toString();
}

private static String issueOrIssues(Collection<?> collection) {
if (collection.size() > 1) {
return "issues";
}
return "issue";
}

private static String issuesOrAnIssue(SetMultimap<Project, ChangedIssue> changedIssues) {
if (changedIssues.size() > 1) {
return "issues";

+ 70
- 5
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java View File

@@ -31,10 +31,13 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.rules.RuleType;
import org.sonar.core.i18n.I18n;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
@@ -42,7 +45,13 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule

import static java.net.URLEncoder.encode;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.function.Function.identity;
import static org.sonar.core.util.stream.MoreCollectors.index;
import static org.sonar.server.issue.notification.RuleGroup.ISSUES;
import static org.sonar.server.issue.notification.RuleGroup.SECURITY_HOTSPOTS;
import static org.sonar.server.issue.notification.RuleGroup.formatIssueOrHotspot;
import static org.sonar.server.issue.notification.RuleGroup.formatIssuesOrHotspots;
import static org.sonar.server.issue.notification.RuleGroup.resolveGroup;

public abstract class IssueChangesEmailTemplate implements EmailTemplate {

@@ -50,6 +59,7 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
private static final Comparator<Project> PROJECT_COMPARATOR = Comparator.comparing(Project::getProjectName)
.thenComparing(t -> t.getBranchName().orElse(""));
private static final Comparator<ChangedIssue> CHANGED_ISSUE_KEY_COMPARATOR = Comparator.comparing(ChangedIssue::getKey, Comparator.naturalOrder());

/**
* Assuming:
* <ul>
@@ -97,6 +107,38 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
});
}

void addIssuesAndHotspotsByProjectThenRule(StringBuilder sb, SetMultimap<Project, ChangedIssue> issuesByProject) {
issuesByProject.keySet().stream()
.sorted(PROJECT_COMPARATOR)
.forEach(project -> {
String encodedProjectParams = toUrlParams(project);
paragraph(sb, s -> toString(s, project));

Set<ChangedIssue> changedIssues = issuesByProject.get(project);
ListMultimap<RuleGroup, ChangedIssue> issuesAndHotspots = changedIssues.stream()
.collect(index(changedIssue -> resolveGroup(changedIssue.getRule().getRuleType()), identity()));

List<ChangedIssue> issues = issuesAndHotspots.get(ISSUES);
List<ChangedIssue> hotspots = issuesAndHotspots.get(SECURITY_HOTSPOTS);

boolean hasSecurityHotspots = !hotspots.isEmpty();
boolean hasOtherIssues = !issues.isEmpty();

if (hasOtherIssues) {
addIssuesByRule(sb, issues, projectIssuePageHref(encodedProjectParams));
}

if (hasSecurityHotspots && hasOtherIssues) {
paragraph(sb, stringBuilder -> {
});
}

if (hasSecurityHotspots) {
addIssuesByRule(sb, hotspots, securityHotspotPageHref(encodedProjectParams));
}
});
}

void addIssuesByRule(StringBuilder sb, Collection<ChangedIssue> changedIssues, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref) {
ListMultimap<Rule, ChangedIssue> issuesByRule = changedIssues.stream()
.collect(index(ChangedIssue::getRule, t -> t));
@@ -114,21 +156,22 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
Collection<ChangedIssue> issues = issuesByRule.get(rule);

sb.append("<li>").append("Rule ").append(" <em>").append(rule.getName()).append("</em> - ");
appendIssueLinks(sb, issuePageHref, issues);
appendIssueLinks(sb, issuePageHref, issues, rule.getRuleType());
sb.append("</li>");
}
sb.append("</ul>");
}

private static void appendIssueLinks(StringBuilder sb, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref, Collection<ChangedIssue> issues) {
private static void appendIssueLinks(StringBuilder sb, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref, Collection<ChangedIssue> issues,
@Nullable RuleType ruleType) {
SortedSet<ChangedIssue> sortedIssues = ImmutableSortedSet.copyOf(CHANGED_ISSUE_KEY_COMPARATOR, issues);
int issueCount = issues.size();
if (issueCount == 1) {
link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See the single issue"));
link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See the single ").append(formatIssueOrHotspot(ruleType)));
} else if (issueCount <= MAX_ISSUES_BY_LINK) {
link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See all ").append(issueCount).append(" issues"));
link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See all ").append(issueCount).append(" ").append(formatIssuesOrHotspots(ruleType)));
} else {
sb.append("See issues");
sb.append("See ").append(formatIssuesOrHotspots(ruleType));
List<List<ChangedIssue>> issueGroups = Lists.partition(ImmutableList.copyOf(sortedIssues), MAX_ISSUES_BY_LINK);
Iterator<List<ChangedIssue>> issueGroupsIterator = issueGroups.iterator();
int[] groupIndex = new int[] {0};
@@ -160,6 +203,21 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
};
}

BiConsumer<StringBuilder, Collection<ChangedIssue>> securityHotspotPageHref(String projectParams) {
return (s, issues) -> {
s.append(settings.getServerBaseURL()).append("/security_hotspots?").append(projectParams)
.append("&hotspots=");

Iterator<ChangedIssue> issueIterator = issues.iterator();
while (issueIterator.hasNext()) {
s.append(urlEncode(issueIterator.next().getKey()));
if (issueIterator.hasNext()) {
s.append(URL_ENCODED_COMMA);
}
}
};
}

private static Consumer<StringBuilder> issueGroupLabel(StringBuilder sb, int[] groupIndex, List<ChangedIssue> issueGroup) {
return s -> {
int firstIssueNumber = (groupIndex[0] * MAX_ISSUES_BY_LINK) + 1;
@@ -207,4 +265,11 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
}
}

protected static String issueOrIssues(Collection<?> collection) {
if (collection.size() > 1) {
return "issues";
}
return "issue";
}

}

+ 16
- 3
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java View File

@@ -27,6 +27,7 @@ import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
@@ -233,10 +234,16 @@ public class IssuesChangesNotificationBuilder {
@Immutable
public static final class Rule {
private final RuleKey key;
private final RuleType ruleType;
private final String name;

public Rule(RuleKey key, String name) {
public Rule(RuleKey key, @Nullable String ruleType, String name) {
this(key, ruleType != null ? RuleType.valueOf(ruleType) : null, name);
}

public Rule(RuleKey key, @Nullable RuleType ruleType, String name) {
this.key = requireNonNull(key, KEY_CANT_BE_NULL_MESSAGE);
this.ruleType = ruleType;
this.name = requireNonNull(name, "name can't be null");
}

@@ -244,6 +251,11 @@ public class IssuesChangesNotificationBuilder {
return key;
}

@CheckForNull
public RuleType getRuleType() {
return ruleType;
}

public String getName() {
return name;
}
@@ -257,18 +269,19 @@ public class IssuesChangesNotificationBuilder {
return false;
}
Rule that = (Rule) o;
return key.equals(that.key) && name.equals(that.name);
return key.equals(that.key) && ruleType.equals(that.ruleType) && name.equals(that.name);
}

@Override
public int hashCode() {
return Objects.hash(key, name);
return Objects.hash(key, ruleType, name);
}

@Override
public String toString() {
return "Rule{" +
"key=" + key +
", type=" + ruleType +
", name='" + name + '\'' +
'}';
}

+ 9
- 3
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java View File

@@ -35,6 +35,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Optional.ofNullable;
import static org.sonar.core.util.stream.MoreCollectors.toSet;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;

@@ -44,6 +45,7 @@ public class IssuesChangesNotificationSerializer {
private static final String FIELD_CHANGE_AUTHOR_UUID = "change.author.uuid";
private static final String FIELD_CHANGE_AUTHOR_LOGIN = "change.author.login";
private static final String FIELD_CHANGE_AUTHOR_NAME = "change.author.name";
private static final String FIELD_PREFIX_RULES = "rules.";

public IssuesChangesNotification serialize(IssuesChangesNotificationBuilder builder) {
IssuesChangesNotification res = new IssuesChangesNotification();
@@ -160,7 +162,10 @@ public class IssuesChangesNotificationSerializer {
issues.stream()
.map(ChangedIssue::getRule)
.collect(Collectors.toSet())
.forEach(rule -> res.setFieldValue("rules." + rule.getKey(), rule.getName()));
.forEach(rule -> {
res.setFieldValue(FIELD_PREFIX_RULES + rule.getKey(), rule.getName());
ofNullable(rule.getRuleType()).ifPresent(ruleType -> res.setFieldValue(FIELD_PREFIX_RULES + rule.getKey() + ".type", rule.getRuleType().name()));
});
}

private static Map<RuleKey, Rule> readRules(IssuesChangesNotification notification, List<Issue> issues) {
@@ -173,10 +178,11 @@ public class IssuesChangesNotificationSerializer {
}

private static Rule readRule(IssuesChangesNotification notification, RuleKey ruleKey) {
String fieldName = "rules." + ruleKey;
String fieldName = FIELD_PREFIX_RULES + ruleKey;
String ruleName = notification.getFieldValue(fieldName);
String ruleType = notification.getFieldValue(fieldName + ".type");
checkState(ruleName != null, "can not find field %s", ruleKey);
return new Rule(ruleKey, ruleName);
return new Rule(ruleKey, ruleType, ruleName);
}

private static void serializeProjects(IssuesChangesNotification res, Set<ChangedIssue> issues) {

+ 65
- 0
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/RuleGroup.java View File

@@ -0,0 +1,65 @@
/*
* SonarQube
* Copyright (C) 2009-2020 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.issue.notification;

import java.util.Collection;
import javax.annotation.Nullable;
import org.sonar.api.rules.RuleType;

import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;

enum RuleGroup {
SECURITY_HOTSPOTS,
ISSUES;

static RuleGroup resolveGroup(@Nullable RuleType ruleType) {
return SECURITY_HOTSPOT.equals(ruleType) ? SECURITY_HOTSPOTS : ISSUES;
}

static String formatIssuesOrHotspots(Collection<?> issues, Collection<?> hotspots) {
if (!issues.isEmpty() && !hotspots.isEmpty()) {
return "issues/hotspots";
}
if (issues.size() == 1) {
return "an issue";
}
if (issues.size() > 1) {
return "issues";
}
if (hotspots.size() == 1) {
return "a hotspot";
}
return "hotspots";
}

static String formatIssueOrHotspot(@Nullable RuleType ruleType) {
if (SECURITY_HOTSPOT.equals(ruleType)) {
return "hotspot";
}
return "issue";
}

static String formatIssuesOrHotspots(@Nullable RuleType ruleType) {
if (SECURITY_HOTSPOT.equals(ruleType)) {
return "hotspots";
}
return "issues";
}
}

+ 4
- 4
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandlerTest.java View File

@@ -36,7 +36,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.sonar.api.rule.RuleKey;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
@@ -61,6 +60,7 @@ import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.core.util.stream.MoreCollectors.index;
import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
@@ -354,10 +354,10 @@ public class ChangesOnMyIssueNotificationHandlerTest {
.collect(toSet());
when(notificationManager.findSubscribedEmailRecipients(
CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project1.getKey(), ImmutableSet.of(assignee1.getLogin()), ALL_MUST_HAVE_ROLE_USER))
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin())));
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin())));
when(notificationManager.findSubscribedEmailRecipients(
CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project2.getKey(), ImmutableSet.of(assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER))
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee2.getLogin())));
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee2.getLogin())));
int deliveredCount = new Random().nextInt(100);
when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount);

@@ -437,7 +437,7 @@ public class ChangesOnMyIssueNotificationHandlerTest {
}

private static Rule newRule() {
return new Rule(RuleKey.of(randomAlphabetic(3), randomAlphabetic(4)), randomAlphabetic(5));
return newRandomNotAHotspotRule(randomAlphabetic(5));
}

private static Set<IssuesChangesNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) {

+ 214
- 75
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplateTest.java View File

@@ -32,11 +32,13 @@ import java.util.Set;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.elasticsearch.common.util.set.Sets;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification;
import org.sonar.api.rules.RuleType;
import org.sonar.core.i18n.I18n;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
@@ -60,14 +62,24 @@ import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.api.issue.Issue.STATUS_REOPENED;
import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newAnalysisChange;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newBranch;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newChangedIssue;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRule;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newSecurityHotspotRule;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newUserChange;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.randomRuleTypeHotspotExcluded;

@RunWith(DataProviderRunner.class)
public class ChangesOnMyIssuesEmailTemplateTest {
private static final String[] ISSUE_STATUSES = {STATUS_OPEN, STATUS_RESOLVED, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_CLOSED};
private static final String[] SECURITY_HOTSPOTS_STATUSES = {STATUS_TO_REVIEW, STATUS_REVIEWED};

@org.junit.Rule
public ExpectedException expectedException = ExpectedException.none();

@@ -84,7 +96,7 @@ public class ChangesOnMyIssuesEmailTemplateTest {

@Test
public void formats_fails_with_ISE_if_change_from_Analysis_and_no_issue() {
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();

expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("changedIssues can't be empty");
@@ -95,9 +107,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
@Test
public void format_sets_message_id_with_project_key_of_first_issue_in_set_when_change_from_Analysis() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
.collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));

@@ -107,7 +119,7 @@ public class ChangesOnMyIssuesEmailTemplateTest {
@Test
public void format_sets_subject_with_project_name_of_first_issue_in_set_when_change_from_Analysis() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
.collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();

@@ -120,9 +132,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
@Test
public void format_sets_subject_with_project_name_and_branch_name_of_first_issue_in_set_when_change_from_Analysis() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newBranch("prj_" + i, "br_" + i), newRule("rule_" + i)))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newBranch("prj_" + i, "br_" + i), newRandomNotAHotspotRule("rule_" + i)))
.collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));

@@ -133,9 +145,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
@Test
public void format_set_html_message_with_header_dealing_with_plural_when_change_from_Analysis() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
.collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();

EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues.stream().limit(1).collect(toSet())));
EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
@@ -151,9 +163,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
@Test
public void format_sets_static_message_id_when_change_from_User() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
.collect(toSet());
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));

@@ -163,21 +175,21 @@ public class ChangesOnMyIssuesEmailTemplateTest {
@Test
public void format_sets_static_subject_when_change_from_User() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
.collect(toSet());
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));

assertThat(emailMessage.getSubject()).isEqualTo("A manual update has changed some of your issues");
assertThat(emailMessage.getSubject()).isEqualTo("A manual update has changed some of your issues/hotspots");
}

@Test
public void format_set_html_message_with_header_dealing_with_plural_when_change_from_User() {
public void format_set_html_message_with_header_dealing_with_plural_issues_when_change_from_User() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
.collect(toSet());
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();

EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
userChange, changedIssues.stream().limit(1).collect(toSet())));
@@ -195,26 +207,91 @@ public class ChangesOnMyIssuesEmailTemplateTest {
.withoutLink();
}

@Test
public void format_set_html_message_with_header_dealing_with_plural_security_hotspots_when_change_from_User() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newSecurityHotspotRule("rule_" + i)))
.collect(toSet());
UserChange userChange = newUserChange();

EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
userChange, changedIssues.stream().limit(1).collect(toSet())));
EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));

HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
.hasParagraph("Hi,")
.withoutLink()
.hasParagraph("A manual change has updated a hotspot assigned to you:")
.withoutLink();
HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
.hasParagraph("Hi,")
.withoutLink()
.hasParagraph("A manual change has updated hotspots assigned to you:")
.withoutLink();
}

@Test
public void format_set_html_message_with_header_dealing_with_plural_security_hotspots_and_issues_when_change_from_User() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
.collect(toSet());

Set<ChangedIssue> changedHotspots = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newSecurityHotspotRule("rule_" + i)))
.collect(toSet());

Set<ChangedIssue> issuesAndHotspots = Sets.union(changedIssues, changedHotspots);

UserChange userChange = newUserChange();

EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, issuesAndHotspots));

HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
.hasParagraph("Hi,")
.withoutLink()
.hasParagraph("A manual change has updated issues/hotspots assigned to you:")
.withoutLink();
}

@Test
@UseDataProvider("issueStatuses")
public void format_set_html_message_with_footer_when_change_from_user(String issueStatus) {
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
public void format_set_html_message_with_footer_when_issue_change_from_user(String issueStatus) {
UserChange userChange = newUserChange();
format_set_html_message_with_footer(userChange, issueStatus, c -> c
// skip content
.hasParagraph() // open/closed issue
.hasList() // rule list
);
.hasParagraph() // skip project header
.hasList(), // rule list,
randomRuleTypeHotspotExcluded());
}

@Test
@UseDataProvider("issueStatuses")
public void format_set_html_message_with_footer_when_change_from_analysis(String issueStatus) {
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
public void format_set_html_message_with_footer_when_issue_change_from_analysis(String issueStatus) {
AnalysisChange analysisChange = newAnalysisChange();
format_set_html_message_with_footer(analysisChange, issueStatus, c -> c
// skip content
.hasParagraph() // status
.hasList() // rule list
);
.hasList(), // rule list,
randomRuleTypeHotspotExcluded());
}

@Test
@UseDataProvider("securityHotspotsStatuses")
public void format_set_html_message_with_footer_when_security_hotspot_change_from_analysis(String securityHotspotStatus) {
AnalysisChange analysisChange = newAnalysisChange();
format_set_html_message_with_footer(analysisChange, securityHotspotStatus, c -> c
.hasParagraph()
.hasList(), // rule list
SECURITY_HOTSPOT);
}

@Test
@UseDataProvider("securityHotspotsStatuses")
public void format_set_html_message_with_footer_when_security_hotspot_change_from_user(String securityHotspotStatus) {
UserChange userChange = newUserChange();
format_set_html_message_with_footer(userChange, securityHotspotStatus, c -> c
.hasParagraph()
.hasList(), // rule list
SECURITY_HOTSPOT);
}

@DataProvider
@@ -224,14 +301,21 @@ public class ChangesOnMyIssuesEmailTemplateTest {
.toArray(Object[][]::new);
}

private void format_set_html_message_with_footer(Change change, String issueStatus, Function<HtmlParagraphAssert, HtmlListAssert> skipContent) {
@DataProvider
public static Object[][] securityHotspotsStatuses() {
return Arrays.stream(SECURITY_HOTSPOTS_STATUSES)
.map(t -> new Object[] {t})
.toArray(Object[][]::new);
}

private void format_set_html_message_with_footer(Change change, String issueStatus, Function<HtmlParagraphAssert, HtmlListAssert> skipContent, RuleType ruleType) {
String wordingNotification = randomAlphabetic(20);
String host = randomAlphabetic(15);
when(i18n.message(Locale.ENGLISH, "notification.dispatcher.ChangesOnMyIssue", "notification.dispatcher.ChangesOnMyIssue"))
.thenReturn(wordingNotification);
when(emailSettings.getServerBaseURL()).thenReturn(host);
Project project = newProject("foo");
Rule rule = newRule("bar");
Rule rule = newRule("bar", ruleType);
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", issueStatus, project, rule))
.collect(toSet());
@@ -260,11 +344,11 @@ public class ChangesOnMyIssuesEmailTemplateTest {
@Test
public void format_set_html_message_with_issues_grouped_by_status_closed_or_any_other_when_change_from_analysis() {
Project project = newProject("foo");
Rule rule = newRule("bar");
Rule rule = newRandomNotAHotspotRule("bar");
Set<ChangedIssue> changedIssues = Arrays.stream(ISSUE_STATUSES)
.map(status -> newChangedIssue(status + "", status, project, rule))
.collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));

@@ -282,16 +366,16 @@ public class ChangesOnMyIssuesEmailTemplateTest {
}

@Test
public void format_set_html_message_with_status_title_handles_plural_when_change_from_analysis() {
public void format_set_html_message_with_issue_status_title_handles_plural_when_change_from_analysis() {
Project project = newProject("foo");
Rule rule = newRule("bar");
Rule rule = newRandomNotAHotspotRule("bar");
Set<ChangedIssue> closedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(status -> newChangedIssue(status + "", STATUS_CLOSED, project, rule))
.collect(toSet());
Set<ChangedIssue> openIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(status -> newChangedIssue(status + "", STATUS_OPEN, project, rule))
.collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();

EmailMessage closedIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, closedIssues));
EmailMessage openIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, openIssues));
@@ -313,8 +397,8 @@ public class ChangesOnMyIssuesEmailTemplateTest {
Project project = newProject("1");
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName);
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
AnalysisChange analysisChange = newAnalysisChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
@@ -333,8 +417,8 @@ public class ChangesOnMyIssuesEmailTemplateTest {
Project project = newProject("1");
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName);
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
@@ -355,8 +439,8 @@ public class ChangesOnMyIssuesEmailTemplateTest {
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
String key = "key";
ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName);
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
AnalysisChange analysisChange = newAnalysisChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
@@ -378,8 +462,8 @@ public class ChangesOnMyIssuesEmailTemplateTest {
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
String key = "key";
ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName);
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
@@ -399,12 +483,12 @@ public class ChangesOnMyIssuesEmailTemplateTest {
Project project = newProject("1");
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
Rule rule = newRule(ruleName);
Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
String issueStatus = randomValidStatus();
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, issueStatus, project, rule))
.collect(toList());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
@@ -426,11 +510,11 @@ public class ChangesOnMyIssuesEmailTemplateTest {
Project project = newProject("1");
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
Rule rule = newRule(ruleName);
Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
.collect(toList());
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
@@ -453,12 +537,12 @@ public class ChangesOnMyIssuesEmailTemplateTest {
Project project = newBranch("1", branchName);
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
Rule rule = newRule(ruleName);
Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
String status = randomValidStatus();
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, status, project, rule))
.collect(toList());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
@@ -481,11 +565,11 @@ public class ChangesOnMyIssuesEmailTemplateTest {
Project project = newBranch("1", branchName);
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
Rule rule = newRule(ruleName);
Rule rule = newRandomNotAHotspotRule(ruleName);
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
.collect(toList());
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
@@ -512,10 +596,10 @@ public class ChangesOnMyIssuesEmailTemplateTest {
Project project3 = newProject("C");
String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3)
.map(project -> newChangedIssue("issue_" + project.getUuid(), randomValidStatus(), project, newRule(randomAlphabetic(2))))
.map(project -> newChangedIssue("issue_" + project.getUuid(), randomValidStatus(), project, newRule(randomAlphabetic(2), randomRuleTypeHotspotExcluded())))
.collect(toList());
Collections.shuffle(changedIssues);
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
@@ -541,17 +625,18 @@ public class ChangesOnMyIssuesEmailTemplateTest {
@Test
public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change() {
Project project = newProject("1");
Rule rule1 = newRule("1");
Rule rule2 = newRule("a");
Rule rule3 = newRule("b");
Rule rule4 = newRule("X");
Rule rule1 = newRandomNotAHotspotRule("1");
Rule rule2 = newRandomNotAHotspotRule("a");
Rule rule3 = newRandomNotAHotspotRule("b");
Rule rule4 = newRandomNotAHotspotRule("X");

String host = randomAlphabetic(15);
String issueStatus = randomValidStatus();
List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
.map(rule -> newChangedIssue("issue_" + rule.getName(), issueStatus, project, rule))
.collect(toList());
Collections.shuffle(changedIssues);
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
@@ -569,30 +654,43 @@ public class ChangesOnMyIssuesEmailTemplateTest {
}

@Test
public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change_when_user_analysis() {
public void formats_returns_html_message_with_rules_ordered_by_name_user_change() {
Project project = newProject("1");
Rule rule1 = newRule("1");
Rule rule2 = newRule("a");
Rule rule3 = newRule("b");
Rule rule4 = newRule("X");
Rule rule1 = newRandomNotAHotspotRule("1");
Rule rule2 = newRandomNotAHotspotRule("a");
Rule rule3 = newRandomNotAHotspotRule("b");
Rule rule4 = newRandomNotAHotspotRule("X");

Rule hotspot1 = newSecurityHotspotRule("S");
Rule hotspot2 = newSecurityHotspotRule("Z");
Rule hotspot3 = newSecurityHotspotRule("N");
Rule hotspot4 = newSecurityHotspotRule("M");

String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4, hotspot1, hotspot2, hotspot3, hotspot4)
.map(rule -> newChangedIssue("issue_" + rule.getName(), randomValidStatus(), project, rule))
.collect(toList());
Collections.shuffle(changedIssues);
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));

HtmlFragmentAssert.assertThat(emailMessage.getMessage())
.hasParagraph().hasParagraph() // skip header
.hasParagraph(project.getProjectName())
.hasParagraph()
.hasParagraph()
.hasParagraph() // skip project name
.hasList(
"Rule " + rule1.getName() + " - See the single issue",
"Rule " + rule2.getName() + " - See the single issue",
"Rule " + rule3.getName() + " - See the single issue",
"Rule " + rule4.getName() + " - See the single issue")
.hasEmptyParagraph()
.hasList(
"Rule " + hotspot1.getName() + " - See the single hotspot",
"Rule " + hotspot2.getName() + " - See the single hotspot",
"Rule " + hotspot3.getName() + " - See the single hotspot",
"Rule " + hotspot4.getName() + " - See the single hotspot")
.hasParagraph().hasParagraph() // skip footer
.noMoreBlock();
}
@@ -600,11 +698,13 @@ public class ChangesOnMyIssuesEmailTemplateTest {
@Test
public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_analysis_change() {
Project project1 = newProject("1");
Rule rule1 = newRule("1");
Rule rule2 = newRule("a");
Rule rule1 = newRandomNotAHotspotRule("1");
Rule rule2 = newRandomNotAHotspotRule("a");

String host = randomAlphabetic(15);
String issueStatusClosed = STATUS_CLOSED;
String otherIssueStatus = STATUS_RESOLVED;

List<ChangedIssue> changedIssues = Stream.of(
IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, issueStatusClosed, project1, rule1)),
IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, issueStatusClosed, project1, rule2)),
@@ -612,8 +712,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, otherIssueStatus, project1, rule1)))
.flatMap(t -> t)
.collect(toList());

Collections.shuffle(changedIssues);
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
@@ -651,23 +752,32 @@ public class ChangesOnMyIssuesEmailTemplateTest {
}

@Test
public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_user_change() {
public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_and_hotspots_when_user_change() {
Project project1 = newProject("1");
Project project2 = newProject("V");
Project project2Branch = newBranch("V", "AB");
Rule rule1 = newRule("1");
Rule rule2 = newRule("a");
Rule rule1 = newRule("1", randomRuleTypeHotspotExcluded());
Rule rule2 = newRule("a", randomRuleTypeHotspotExcluded());

Rule hotspot1 = newSecurityHotspotRule("h1");
Rule hotspot2 = newSecurityHotspotRule("h2");

String status = randomValidStatus();
String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of(
IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, status, project1, rule1)),
IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, status, project1, rule2)),
IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, status, project2, rule2)),
IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, status, project2Branch, rule1)))
IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, status, project2Branch, rule1)),

IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, STATUS_REVIEWED, project1, hotspot1)),
IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, STATUS_REVIEWED, project1, hotspot2)),
IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, STATUS_TO_REVIEW, project2, hotspot2)),
IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, STATUS_TO_REVIEW, project2Branch, hotspot1)))
.flatMap(t -> t)
.collect(toList());
Collections.shuffle(changedIssues);
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
@@ -685,8 +795,20 @@ public class ChangesOnMyIssuesEmailTemplateTest {
.withLink("See all 40 issues",
host + "/project/issues?id=" + project1.getKey()
+ "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
.hasEmptyParagraph()
.hasList()
.withItemTexts(
"Rule " + hotspot1.getName() + " - See all 39 hotspots",
"Rule " + hotspot2.getName() + " - See all 40 hotspots")
.withLink("See all 39 hotspots",
host + "/security_hotspots?id=" + project1.getKey()
+ "&hotspots=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
.withLink("See all 40 hotspots",
host + "/security_hotspots?id=" + project1.getKey()
+ "&hotspots=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
.hasParagraph(project2.getProjectName())
.hasList("Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
.hasList(
"Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
.withLink("1-40",
host + "/project/issues?id=" + project2.getKey()
+ "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
@@ -696,11 +818,28 @@ public class ChangesOnMyIssuesEmailTemplateTest {
.withLink("81",
host + "/project/issues?id=" + project2.getKey()
+ "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
.hasEmptyParagraph()
.hasList("Rule " + hotspot2.getName() + " - See hotspots 1-40 41-80 81")
.withLink("1-40",
host + "/security_hotspots?id=" + project2.getKey()
+ "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
.withLink("41-80",
host + "/security_hotspots?id=" + project2.getKey()
+ "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
.withLink("81",
host + "/security_hotspots?id=" + project2.getKey()
+ "&hotspots=" + "1-40_41-80_1_9")
.hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get())
.hasList("Rule " + rule1.getName() + " - See all 6 issues")
.hasList(
"Rule " + rule1.getName() + " - See all 6 issues")
.withLink("See all 6 issues",
host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
+ "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
.hasEmptyParagraph()
.hasList("Rule " + hotspot1.getName() + " - See all 6 hotspots")
.withLink("See all 6 hotspots",
host + "/security_hotspots?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
+ "&hotspots=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
.hasParagraph().hasParagraph() // skip footer
.noMoreBlock();
}

+ 5
- 2
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotificationTest.java View File

@@ -31,6 +31,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;

public class ChangesOnMyIssuesNotificationTest {
@Test
@@ -45,7 +46,8 @@ public class ChangesOnMyIssuesNotificationTest {
@Test
public void equals_is_based_on_change_and_issues() {
AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"), IssuesChangesNotificationBuilderTesting.newRule("rul"));
ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"),
newRandomNotAHotspotRule("rul"));
ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue));

assertThat(underTest)
@@ -59,7 +61,8 @@ public class ChangesOnMyIssuesNotificationTest {
@Test
public void hashcode_is_based_on_change_and_issues() {
AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"), IssuesChangesNotificationBuilderTesting.newRule("rul"));
ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"),
newRandomNotAHotspotRule("rul"));
ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue));

assertThat(underTest.hashCode())

+ 2
- 3
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandlerTest.java View File

@@ -34,12 +34,10 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.sonar.api.issue.Issue;
import org.sonar.api.rule.RuleKey;
import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
import org.sonar.server.notification.NotificationDispatcherMetadata;
@@ -66,6 +64,7 @@ import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
import static org.sonar.core.util.stream.MoreCollectors.index;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
@@ -477,7 +476,7 @@ public class FPOrWontFixNotificationHandlerTest {
.setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5)))
.setNewStatus(randomAlphabetic(12))
.setNewResolution(randomAlphabetic(13))
.setRule(new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), randomAlphabetic(8)))
.setRule(newRandomNotAHotspotRule(randomAlphabetic(8)))
.setProject(new Project.Builder(randomAlphabetic(9))
.setKey(randomAlphabetic(10))
.setProjectName(randomAlphabetic(11))

+ 6
- 5
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationTest.java View File

@@ -26,7 +26,6 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.Test;
import org.sonar.api.rule.RuleKey;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
@@ -36,18 +35,19 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User
import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP;
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;

public class FPOrWontFixNotificationTest {
@Test
public void equals_is_based_on_issues_change_and_resolution() {
Rule rule = new Rule(RuleKey.of("repo", "rule_key"), "rule_name");
Rule rule = newRandomNotAHotspotRule("rule_name");
Project project = new Project.Builder("prj_uuid").setKey("prj_key").setProjectName("prj_name").build();
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> new ChangedIssue.Builder("key_" + i)
.setNewStatus("status")
.setRule(rule)
.setProject(project)
.build())
.build())
.collect(Collectors.toSet());
AnalysisChange change = new AnalysisChange(12);
User user = new User("uuid", "login", null);
@@ -64,16 +64,17 @@ public class FPOrWontFixNotificationTest {
.isNotEqualTo(new FPOrWontFixNotification(new IssuesChangesNotificationBuilder.UserChange(12, user), changedIssues, WONT_FIX))
.isNotEqualTo(new FPOrWontFixNotification(change, changedIssues, FP));
}

@Test
public void hashcode_is_based_on_issues_change_and_resolution() {
Rule rule = new Rule(RuleKey.of("repo", "rule_key"), "rule_name");
Rule rule = newRandomNotAHotspotRule("rule_name");
Project project = new Project.Builder("prj_uuid").setKey("prj_key").setProjectName("prj_name").build();
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> new ChangedIssue.Builder("key_" + i)
.setNewStatus("status")
.setRule(rule)
.setProject(project)
.build())
.build())
.collect(Collectors.toSet());
AnalysisChange change = new AnalysisChange(12);
User user = new User("uuid", "login", null);

+ 116
- 15
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplateTest.java View File

@@ -34,6 +34,7 @@ import org.junit.runner.RunWith;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.core.i18n.I18n;
import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
@@ -51,8 +52,12 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP;
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newSecurityHotspotRule;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.randomRuleTypeHotspotExcluded;

@RunWith(DataProviderRunner.class)
public class FpOrWontFixEmailTemplateTest {
@@ -160,7 +165,7 @@ public class FpOrWontFixEmailTemplateTest {
Project project = newProject("1");
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
ChangedIssue changedIssue = newChangedIssue("key", project, ruleName);
ChangedIssue changedIssue = newChangedIssue("key", project, ruleName, randomRuleTypeHotspotExcluded());
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
@@ -174,6 +179,26 @@ public class FpOrWontFixEmailTemplateTest {
.noMoreBlock();
}

@Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_single_hotspot_on_master(Change change, FpOrWontFix fpOrWontFix) {
Project project = newProject("1");
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
ChangedIssue changedIssue = newChangedIssue("key", project, ruleName, SECURITY_HOTSPOT);
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));

HtmlFragmentAssert.assertThat(emailMessage.getMessage())
.hasParagraph().hasParagraph() // skip header
.hasParagraph(project.getProjectName())
.hasList("Rule " + ruleName + " - See the single hotspot")
.withLink("See the single hotspot", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
.hasParagraph().hasParagraph() // skip footer
.noMoreBlock();
}

@Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_single_issue_on_branch(Change change, FpOrWontFix fpOrWontFix) {
@@ -182,7 +207,7 @@ public class FpOrWontFixEmailTemplateTest {
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
String key = "key";
ChangedIssue changedIssue = newChangedIssue(key, project, ruleName);
ChangedIssue changedIssue = newChangedIssue(key, project, ruleName, randomRuleTypeHotspotExcluded());
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
@@ -197,13 +222,36 @@ public class FpOrWontFixEmailTemplateTest {
.noMoreBlock();
}

@Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_single_hotspot_on_branch(Change change, FpOrWontFix fpOrWontFix) {
String branchName = randomAlphabetic(6);
Project project = newBranch("1", branchName);
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
String key = "key";
ChangedIssue changedIssue = newChangedIssue(key, project, ruleName, SECURITY_HOTSPOT);
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));

HtmlFragmentAssert.assertThat(emailMessage.getMessage())
.hasParagraph().hasParagraph() // skip header
.hasParagraph(project.getProjectName() + ", " + branchName)
.hasList("Rule " + ruleName + " - See the single hotspot")
.withLink("See the single hotspot",
host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
.hasParagraph().hasParagraph() // skip footer
.noMoreBlock();
}

@Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master(Change change, FpOrWontFix fpOrWontFix) {
Project project = newProject("1");
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
Rule rule = newRule(ruleName);
Rule rule = newRandomNotAHotspotRule(ruleName);
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
.collect(toList());
@@ -223,6 +271,32 @@ public class FpOrWontFixEmailTemplateTest {
.noMoreBlock();
}

@Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_multiple_hotspots_of_same_rule_on_same_project_on_master(Change change, FpOrWontFix fpOrWontFix) {
Project project = newProject("1");
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
Rule rule = newSecurityHotspotRule(ruleName);
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
.collect(toList());
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix));

String expectedHref = host + "/project/issues?id=" + project.getKey()
+ "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
String expectedLinkText = "See all " + changedIssues.size() + " hotspots";
HtmlFragmentAssert.assertThat(emailMessage.getMessage())
.hasParagraph().hasParagraph() // skip header
.hasParagraph(project.getProjectName())
.hasList("Rule " + ruleName + " - " + expectedLinkText)
.withLink(expectedLinkText, expectedHref)
.hasParagraph().hasParagraph() // skip footer
.noMoreBlock();
}

@Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch(Change change, FpOrWontFix fpOrWontFix) {
@@ -230,7 +304,7 @@ public class FpOrWontFixEmailTemplateTest {
Project project = newBranch("1", branchName);
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
Rule rule = newRule(ruleName);
Rule rule = newRandomNotAHotspotRule(ruleName);
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
.collect(toList());
@@ -250,6 +324,33 @@ public class FpOrWontFixEmailTemplateTest {
.noMoreBlock();
}

@Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_multiple_hotspots_of_same_rule_on_same_project_on_branch(Change change, FpOrWontFix fpOrWontFix) {
String branchName = randomAlphabetic(19);
Project project = newBranch("1", branchName);
String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15);
Rule rule = newSecurityHotspotRule(ruleName);
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
.collect(toList());
when(emailSettings.getServerBaseURL()).thenReturn(host);

EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix));

String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
+ "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
String expectedLinkText = "See all " + changedIssues.size() + " hotspots";
HtmlFragmentAssert.assertThat(emailMessage.getMessage())
.hasParagraph().hasParagraph() // skip header
.hasParagraph(project.getProjectName() + ", " + branchName)
.hasList("Rule " + ruleName + " - " + expectedLinkText)
.withLink(expectedLinkText, expectedHref)
.hasParagraph().hasParagraph() // skip footer
.noMoreBlock();
}

@Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_with_projects_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) {
@@ -261,7 +362,7 @@ public class FpOrWontFixEmailTemplateTest {
Project project3 = newProject("C");
String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3)
.map(project -> newChangedIssue("issue_" + project.getUuid(), project, newRule(randomAlphabetic(2))))
.map(project -> newChangedIssue("issue_" + project.getUuid(), project, newRandomNotAHotspotRule(randomAlphabetic(2))))
.collect(toList());
Collections.shuffle(changedIssues);
when(emailSettings.getServerBaseURL()).thenReturn(host);
@@ -290,10 +391,10 @@ public class FpOrWontFixEmailTemplateTest {
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_with_rules_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) {
Project project = newProject("1");
Rule rule1 = newRule("1");
Rule rule2 = newRule("a");
Rule rule3 = newRule("b");
Rule rule4 = newRule("X");
Rule rule1 = newRandomNotAHotspotRule("1");
Rule rule2 = newRandomNotAHotspotRule("a");
Rule rule3 = newRandomNotAHotspotRule("b");
Rule rule4 = newRandomNotAHotspotRule("X");
String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
.map(rule -> newChangedIssue("issue_" + rule.getName(), project, rule))
@@ -321,8 +422,8 @@ public class FpOrWontFixEmailTemplateTest {
Project project1 = newProject("1");
Project project2 = newProject("V");
Project project2Branch = newBranch("V", "AB");
Rule rule1 = newRule("1");
Rule rule2 = newRule("a");
Rule rule1 = newRandomNotAHotspotRule("1");
Rule rule2 = newRandomNotAHotspotRule("a");
String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of(
IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, project1, rule1)),
@@ -393,8 +494,8 @@ public class FpOrWontFixEmailTemplateTest {
};
}

private static ChangedIssue newChangedIssue(String key, Project project, String ruleName) {
return newChangedIssue(key, project, newRule(ruleName));
private static ChangedIssue newChangedIssue(String key, Project project, String ruleName, RuleType ruleType) {
return newChangedIssue(key, project, newRule(ruleName, ruleType));
}

private static ChangedIssue newChangedIssue(String key, Project project, Rule rule) {
@@ -405,8 +506,8 @@ public class FpOrWontFixEmailTemplateTest {
.build();
}

private static Rule newRule(String ruleName) {
return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleName);
private static Rule newRule(String ruleName, RuleType ruleType) {
return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleType, ruleName);
}

private static Project newProject(String uuid) {

+ 28
- 6
server/sonar-server-common/src/testFixtures/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilderTesting.java View File

@@ -21,6 +21,7 @@ package org.sonar.server.issue.notification;

import java.util.Random;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.db.DbTester;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
@@ -36,15 +37,24 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User

import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.sonar.api.rules.RuleType.BUG;
import static org.sonar.api.rules.RuleType.CODE_SMELL;
import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.api.rules.RuleType.VULNERABILITY;

public class IssuesChangesNotificationBuilderTesting {

private static final RuleType[] RULE_TYPES = {CODE_SMELL, BUG, VULNERABILITY, SECURITY_HOTSPOT};

private IssuesChangesNotificationBuilderTesting() {
}

public static Rule ruleOf(RuleDto rule) {
return new Rule(rule.getKey(), rule.getName());
return new Rule(rule.getKey(), RuleType.valueOfNullable(rule.getType()), rule.getName());
}

public static Rule ruleOf(RuleDefinitionDto rule) {
return new Rule(rule.getKey(), rule.getName());
return new Rule(rule.getKey(), RuleType.valueOfNullable(rule.getType()), rule.getName());
}

public static User userOf(UserDto changeAuthor) {
@@ -76,8 +86,8 @@ public class IssuesChangesNotificationBuilderTesting {
.build();
}

static ChangedIssue newChangedIssue(String key, String status, Project project, String ruleName) {
return newChangedIssue(key, status, project, newRule(ruleName));
static ChangedIssue newChangedIssue(String key, String status, Project project, String ruleName, RuleType ruleType) {
return newChangedIssue(key, status, project, newRule(ruleName, ruleType));
}

static ChangedIssue newChangedIssue(String key, String status, Project project, Rule rule) {
@@ -88,8 +98,16 @@ public class IssuesChangesNotificationBuilderTesting {
.build();
}

static Rule newRule(String ruleName) {
return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleName);
static Rule newRule(String ruleName, RuleType ruleType) {
return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleType, ruleName);
}

static Rule newRandomNotAHotspotRule(String ruleName) {
return newRule(ruleName, randomRuleTypeHotspotExcluded());
}

static Rule newSecurityHotspotRule(String ruleName) {
return newRule(ruleName, SECURITY_HOTSPOT);
}

static Project newProject(String uuid) {
@@ -107,4 +125,8 @@ public class IssuesChangesNotificationBuilderTesting {
static AnalysisChange newAnalysisChange() {
return new AnalysisChange(new Random().nextLong());
}

static RuleType randomRuleTypeHotspotExcluded() {
return RULE_TYPES[new Random().nextInt(RULE_TYPES.length - 1)];
}
}

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java View File

@@ -306,7 +306,7 @@ public class BulkChangeAction implements IssuesWsAction {
.setNewStatus(issue.status())
.setNewResolution(issue.resolution())
.setAssignee(assignee.map(u -> new User(u.getUuid(), u.getLogin(), u.getName())).orElse(null))
.setRule(new IssuesChangesNotificationBuilder.Rule(ruleDefinitionDto.getKey(), ruleDefinitionDto.getName()))
.setRule(new IssuesChangesNotificationBuilder.Rule(ruleDefinitionDto.getKey(), RuleType.valueOfNullable(ruleDefinitionDto.getType()), ruleDefinitionDto.getName()))
.setProject(new Project.Builder(projectDto.uuid())
.setKey(projectDto.getKey())
.setProjectName(projectDto.name())

+ 2
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java View File

@@ -24,6 +24,7 @@ import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rules.RuleType;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.util.stream.MoreCollectors;
@@ -112,7 +113,7 @@ public class IssueUpdater {
.setNewResolution(issue.resolution())
.setNewStatus(issue.status())
.setAssignee(assignee.map(assigneeDto -> new User(assigneeDto.getUuid(), assigneeDto.getLogin(), assigneeDto.getName())).orElse(null))
.setRule(rule.map(r -> new Rule(r.getKey(), r.getName())).get())
.setRule(rule.map(r -> new Rule(r.getKey(), RuleType.valueOfNullable(r.getType()), r.getName())).get())
.setProject(new Project.Builder(project.uuid())
.setKey(project.getKey())
.setProjectName(project.name())

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

@@ -1608,7 +1608,7 @@ email_configuration.test.email_was_sent_to_x=Email was sent to {0}
#------------------------------------------------------------------------------
notification.channel.EmailNotificationChannel=Email
notification.dispatcher.information=A notification is never sent to the author of the event.
notification.dispatcher.ChangesOnMyIssue=Changes in issues assigned to me
notification.dispatcher.ChangesOnMyIssue=Changes in issues/hotspots assigned to me
notification.dispatcher.NewIssues=New issues
notification.dispatcher.NewAlerts=New quality gate status
notification.dispatcher.NewFalsePositiveIssue=Issues resolved as false positive or won't fix

+ 1
- 2
sonar-plugin-api/src/main/java/org/sonar/api/rules/RuleType.java View File

@@ -61,7 +61,7 @@ public enum RuleType {
}
throw new IllegalArgumentException(format("Unsupported type value : %d", dbConstant));
}
@CheckForNull
public static RuleType valueOfNullable(int dbConstant) {
// iterating the array is fast-enough as size is small. No need for a map.
@@ -75,5 +75,4 @@ public enum RuleType {
}
throw new IllegalArgumentException(format("Unsupported type value : %d", dbConstant));
}

}

Loading…
Cancel
Save