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



private IssuesChangesNotificationBuilder.Rule getRuleByRuleKey(RuleKey ruleKey) { private IssuesChangesNotificationBuilder.Rule getRuleByRuleKey(RuleKey ruleKey) {
return ruleRepository.findByKey(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")); .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

import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when; 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.Component.Type;
import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder; import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
import static org.sonar.ce.task.projectanalysis.step.SendIssueNotificationsStep.NOTIF_TYPES; import static org.sonar.ce.task.projectanalysis.step.SendIssueNotificationsStep.NOTIF_TYPES;
public DbTester db = DbTester.create(System2.INSTANCE); public DbTester db = DbTester.create(System2.INSTANCE);


private final Random random = new Random(); 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)]; private final RuleType randomRuleType = RULE_TYPES_EXCEPT_HOTSPOTS[random.nextInt(RULE_TYPES_EXCEPT_HOTSPOTS.length)];
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Class<Map<String, UserDto>> assigneeCacheType = (Class<Map<String, UserDto>>) (Object) Map.class; private Class<Map<String, UserDto>> assigneeCacheType = (Class<Map<String, UserDto>>) (Object) Map.class;
.isEmpty(); .isEmpty();
Tuple[] expected = stream(users).map(user -> tuple(user.getUuid(), user.getUuid(), user.getId(), user.getLogin())).toArray(Tuple[]::new); Tuple[] expected = stream(users).map(user -> tuple(user.getUuid(), user.getUuid(), user.getId(), user.getLogin())).toArray(Tuple[]::new);
assertThat(cache.entrySet()) 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); .containsOnly(expected);
} }


} }


@Test @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(); UserDto user = db.users().insertUser();
ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName()); ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName());
ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.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



import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import com.google.common.collect.SetMultimap; import com.google.common.collect.SetMultimap;
import java.util.Collection;
import java.util.List; import java.util.List;
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;
import org.sonar.api.config.EmailSettings; import org.sonar.api.config.EmailSettings;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;


import static com.google.common.base.Preconditions.checkState; 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_CLOSED;
import static org.sonar.api.issue.Issue.STATUS_OPEN; import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.core.util.stream.MoreCollectors.index; 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". * Creates email message for notification "Changes on my issues".
return new EmailMessage() return new EmailMessage()
.setFrom(user.getName().orElse(user.getLogin())) .setFrom(user.getName().orElse(user.getLogin()))
.setMessageId("changes-on-my-issues") .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)); .setHtmlMessage(buildMultiProjectMessage(notification));
} }


private String buildMultiProjectMessage(ChangesOnMyIssuesNotification 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(); StringBuilder sb = new StringBuilder();
paragraph(sb, s -> s.append("Hi,")); 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); addFooter(sb, NOTIFICATION_NAME_I18N_KEY);


return sb.toString(); 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) { private static String issuesOrAnIssue(SetMultimap<Project, ChangedIssue> changedIssues) {
if (changedIssues.size() > 1) { if (changedIssues.size() > 1) {
return "issues"; return "issues";

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

import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.sonar.api.config.EmailSettings; import org.sonar.api.config.EmailSettings;
import org.sonar.api.rules.RuleType;
import org.sonar.core.i18n.I18n; import org.sonar.core.i18n.I18n;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;


import static java.net.URLEncoder.encode; import static java.net.URLEncoder.encode;
import static java.nio.charset.StandardCharsets.UTF_8; 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.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 { public abstract class IssueChangesEmailTemplate implements EmailTemplate {


private static final Comparator<Project> PROJECT_COMPARATOR = Comparator.comparing(Project::getProjectName) private static final Comparator<Project> PROJECT_COMPARATOR = Comparator.comparing(Project::getProjectName)
.thenComparing(t -> t.getBranchName().orElse("")); .thenComparing(t -> t.getBranchName().orElse(""));
private static final Comparator<ChangedIssue> CHANGED_ISSUE_KEY_COMPARATOR = Comparator.comparing(ChangedIssue::getKey, Comparator.naturalOrder()); private static final Comparator<ChangedIssue> CHANGED_ISSUE_KEY_COMPARATOR = Comparator.comparing(ChangedIssue::getKey, Comparator.naturalOrder());

/** /**
* Assuming: * Assuming:
* <ul> * <ul>
}); });
} }


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) { void addIssuesByRule(StringBuilder sb, Collection<ChangedIssue> changedIssues, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref) {
ListMultimap<Rule, ChangedIssue> issuesByRule = changedIssues.stream() ListMultimap<Rule, ChangedIssue> issuesByRule = changedIssues.stream()
.collect(index(ChangedIssue::getRule, t -> t)); .collect(index(ChangedIssue::getRule, t -> t));
Collection<ChangedIssue> issues = issuesByRule.get(rule); Collection<ChangedIssue> issues = issuesByRule.get(rule);


sb.append("<li>").append("Rule ").append(" <em>").append(rule.getName()).append("</em> - "); 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("</li>");
} }
sb.append("</ul>"); 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); SortedSet<ChangedIssue> sortedIssues = ImmutableSortedSet.copyOf(CHANGED_ISSUE_KEY_COMPARATOR, issues);
int issueCount = issues.size(); int issueCount = issues.size();
if (issueCount == 1) { 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) { } 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 { } else {
sb.append("See issues");
sb.append("See ").append(formatIssuesOrHotspots(ruleType));
List<List<ChangedIssue>> issueGroups = Lists.partition(ImmutableList.copyOf(sortedIssues), MAX_ISSUES_BY_LINK); List<List<ChangedIssue>> issueGroups = Lists.partition(ImmutableList.copyOf(sortedIssues), MAX_ISSUES_BY_LINK);
Iterator<List<ChangedIssue>> issueGroupsIterator = issueGroups.iterator(); Iterator<List<ChangedIssue>> issueGroupsIterator = issueGroups.iterator();
int[] groupIndex = new int[] {0}; int[] groupIndex = new int[] {0};
}; };
} }


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) { private static Consumer<StringBuilder> issueGroupLabel(StringBuilder sb, int[] groupIndex, List<ChangedIssue> issueGroup) {
return s -> { return s -> {
int firstIssueNumber = (groupIndex[0] * MAX_ISSUES_BY_LINK) + 1; int firstIssueNumber = (groupIndex[0] * MAX_ISSUES_BY_LINK) + 1;
} }
} }


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

import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;


import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
@Immutable @Immutable
public static final class Rule { public static final class Rule {
private final RuleKey key; private final RuleKey key;
private final RuleType ruleType;
private final String name; 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.key = requireNonNull(key, KEY_CANT_BE_NULL_MESSAGE);
this.ruleType = ruleType;
this.name = requireNonNull(name, "name can't be null"); this.name = requireNonNull(name, "name can't be null");
} }


return key; return key;
} }


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

public String getName() { public String getName() {
return name; return name;
} }
return false; return false;
} }
Rule that = (Rule) o; 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 @Override
public int hashCode() { public int hashCode() {
return Objects.hash(key, name);
return Objects.hash(key, ruleType, name);
} }


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

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



import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState; 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.toSet;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;


private static final String FIELD_CHANGE_AUTHOR_UUID = "change.author.uuid"; 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_LOGIN = "change.author.login";
private static final String FIELD_CHANGE_AUTHOR_NAME = "change.author.name"; private static final String FIELD_CHANGE_AUTHOR_NAME = "change.author.name";
private static final String FIELD_PREFIX_RULES = "rules.";


public IssuesChangesNotification serialize(IssuesChangesNotificationBuilder builder) { public IssuesChangesNotification serialize(IssuesChangesNotificationBuilder builder) {
IssuesChangesNotification res = new IssuesChangesNotification(); IssuesChangesNotification res = new IssuesChangesNotification();
issues.stream() issues.stream()
.map(ChangedIssue::getRule) .map(ChangedIssue::getRule)
.collect(Collectors.toSet()) .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) { private static Map<RuleKey, Rule> readRules(IssuesChangesNotification notification, List<Issue> issues) {
} }


private static Rule readRule(IssuesChangesNotification notification, RuleKey ruleKey) { private static Rule readRule(IssuesChangesNotification notification, RuleKey ruleKey) {
String fieldName = "rules." + ruleKey;
String fieldName = FIELD_PREFIX_RULES + ruleKey;
String ruleName = notification.getFieldValue(fieldName); String ruleName = notification.getFieldValue(fieldName);
String ruleType = notification.getFieldValue(fieldName + ".type");
checkState(ruleName != null, "can not find field %s", ruleKey); 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) { 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

/*
* 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

import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.sonar.api.rule.RuleKey;
import org.sonar.core.util.stream.MoreCollectors; import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.sonar.core.util.stream.MoreCollectors.index; import static org.sonar.core.util.stream.MoreCollectors.index;
import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex; 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.GLOBAL_NOTIFICATION;
import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION; import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER; import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
.collect(toSet()); .collect(toSet());
when(notificationManager.findSubscribedEmailRecipients( when(notificationManager.findSubscribedEmailRecipients(
CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project1.getKey(), ImmutableSet.of(assignee1.getLogin()), ALL_MUST_HAVE_ROLE_USER)) 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( when(notificationManager.findSubscribedEmailRecipients(
CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project2.getKey(), ImmutableSet.of(assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER)) 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); int deliveredCount = new Random().nextInt(100);
when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount); when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount);


} }


private static Rule newRule() { 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) { 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

import java.util.function.Function; import java.util.function.Function;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.elasticsearch.common.util.set.Sets;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.sonar.api.config.EmailSettings; import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification; import org.sonar.api.notifications.Notification;
import org.sonar.api.rules.RuleType;
import org.sonar.core.i18n.I18n; import org.sonar.core.i18n.I18n;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
import static org.sonar.api.issue.Issue.STATUS_OPEN; 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_REOPENED;
import static org.sonar.api.issue.Issue.STATUS_RESOLVED; 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.newBranch;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newChangedIssue; 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.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.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) @RunWith(DataProviderRunner.class)
public class ChangesOnMyIssuesEmailTemplateTest { public class ChangesOnMyIssuesEmailTemplateTest {
private static final String[] ISSUE_STATUSES = {STATUS_OPEN, STATUS_RESOLVED, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_CLOSED}; 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 @org.junit.Rule
public ExpectedException expectedException = ExpectedException.none(); public ExpectedException expectedException = ExpectedException.none();




@Test @Test
public void formats_fails_with_ISE_if_change_from_Analysis_and_no_issue() { 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.expect(IllegalStateException.class);
expectedException.expectMessage("changedIssues can't be empty"); expectedException.expectMessage("changedIssues can't be empty");
@Test @Test
public void format_sets_message_id_with_project_key_of_first_issue_in_set_when_change_from_Analysis() { 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)) 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()); .collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();


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


@Test @Test
public void format_sets_subject_with_project_name_of_first_issue_in_set_when_change_from_Analysis() { 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)) 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()); .collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();


@Test @Test
public void format_sets_subject_with_project_name_and_branch_name_of_first_issue_in_set_when_change_from_Analysis() { 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)) 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()); .collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();


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


@Test @Test
public void format_set_html_message_with_header_dealing_with_plural_when_change_from_Analysis() { 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)) 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()); .collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();


EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues.stream().limit(1).collect(toSet()))); EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues.stream().limit(1).collect(toSet())));
EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues)); EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
@Test @Test
public void format_sets_static_message_id_when_change_from_User() { public void format_sets_static_message_id_when_change_from_User() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) 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()); .collect(toSet());
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();


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


@Test @Test
public void format_sets_static_subject_when_change_from_User() { public void format_sets_static_subject_when_change_from_User() {
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) 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()); .collect(toSet());
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();


EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues)); 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 @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)) 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()); .collect(toSet());
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();


EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification( EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
userChange, changedIssues.stream().limit(1).collect(toSet()))); userChange, changedIssues.stream().limit(1).collect(toSet())));
.withoutLink(); .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 @Test
@UseDataProvider("issueStatuses") @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 format_set_html_message_with_footer(userChange, issueStatus, c -> c
// skip content // skip content
.hasParagraph() // open/closed issue
.hasList() // rule list
);
.hasParagraph() // skip project header
.hasList(), // rule list,
randomRuleTypeHotspotExcluded());
} }


@Test @Test
@UseDataProvider("issueStatuses") @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 format_set_html_message_with_footer(analysisChange, issueStatus, c -> c
// skip content
.hasParagraph() // status .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 @DataProvider
.toArray(Object[][]::new); .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 wordingNotification = randomAlphabetic(20);
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
when(i18n.message(Locale.ENGLISH, "notification.dispatcher.ChangesOnMyIssue", "notification.dispatcher.ChangesOnMyIssue")) when(i18n.message(Locale.ENGLISH, "notification.dispatcher.ChangesOnMyIssue", "notification.dispatcher.ChangesOnMyIssue"))
.thenReturn(wordingNotification); .thenReturn(wordingNotification);
when(emailSettings.getServerBaseURL()).thenReturn(host); when(emailSettings.getServerBaseURL()).thenReturn(host);
Project project = newProject("foo"); Project project = newProject("foo");
Rule rule = newRule("bar");
Rule rule = newRule("bar", ruleType);
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
.mapToObj(i -> newChangedIssue(i + "", issueStatus, project, rule)) .mapToObj(i -> newChangedIssue(i + "", issueStatus, project, rule))
.collect(toSet()); .collect(toSet());
@Test @Test
public void format_set_html_message_with_issues_grouped_by_status_closed_or_any_other_when_change_from_analysis() { public void format_set_html_message_with_issues_grouped_by_status_closed_or_any_other_when_change_from_analysis() {
Project project = newProject("foo"); Project project = newProject("foo");
Rule rule = newRule("bar");
Rule rule = newRandomNotAHotspotRule("bar");
Set<ChangedIssue> changedIssues = Arrays.stream(ISSUE_STATUSES) Set<ChangedIssue> changedIssues = Arrays.stream(ISSUE_STATUSES)
.map(status -> newChangedIssue(status + "", status, project, rule)) .map(status -> newChangedIssue(status + "", status, project, rule))
.collect(toSet()); .collect(toSet());
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();


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


} }


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


EmailMessage closedIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, closedIssues)); EmailMessage closedIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, closedIssues));
EmailMessage openIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, openIssues)); EmailMessage openIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, openIssues));
Project project = newProject("1"); Project project = newProject("1");
String ruleName = randomAlphabetic(8); String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15); 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); when(emailSettings.getServerBaseURL()).thenReturn(host);


EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue))); EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
Project project = newProject("1"); Project project = newProject("1");
String ruleName = randomAlphabetic(8); String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15); 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); when(emailSettings.getServerBaseURL()).thenReturn(host);


EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue))); EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
String ruleName = randomAlphabetic(8); String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
String key = "key"; 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); when(emailSettings.getServerBaseURL()).thenReturn(host);


EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue))); EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
String ruleName = randomAlphabetic(8); String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
String key = "key"; 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); when(emailSettings.getServerBaseURL()).thenReturn(host);


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


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


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


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


EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues))); EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
Project project3 = newProject("C"); Project project3 = newProject("C");
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3) 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()); .collect(toList());
Collections.shuffle(changedIssues); Collections.shuffle(changedIssues);
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host); when(emailSettings.getServerBaseURL()).thenReturn(host);


EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues))); EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
@Test @Test
public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change() { public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change() {
Project project = newProject("1"); 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 host = randomAlphabetic(15);
String issueStatus = randomValidStatus(); String issueStatus = randomValidStatus();
List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4) List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
.map(rule -> newChangedIssue("issue_" + rule.getName(), issueStatus, project, rule)) .map(rule -> newChangedIssue("issue_" + rule.getName(), issueStatus, project, rule))
.collect(toList()); .collect(toList());
Collections.shuffle(changedIssues); Collections.shuffle(changedIssues);
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
AnalysisChange analysisChange = newAnalysisChange();
when(emailSettings.getServerBaseURL()).thenReturn(host); when(emailSettings.getServerBaseURL()).thenReturn(host);


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


@Test @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"); 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); 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)) .map(rule -> newChangedIssue("issue_" + rule.getName(), randomValidStatus(), project, rule))
.collect(toList()); .collect(toList());
Collections.shuffle(changedIssues); Collections.shuffle(changedIssues);
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host); when(emailSettings.getServerBaseURL()).thenReturn(host);


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


HtmlFragmentAssert.assertThat(emailMessage.getMessage()) HtmlFragmentAssert.assertThat(emailMessage.getMessage())
.hasParagraph().hasParagraph() // skip header
.hasParagraph(project.getProjectName())
.hasParagraph()
.hasParagraph()
.hasParagraph() // skip project name
.hasList( .hasList(
"Rule " + rule1.getName() + " - See the single issue", "Rule " + rule1.getName() + " - See the single issue",
"Rule " + rule2.getName() + " - See the single issue", "Rule " + rule2.getName() + " - See the single issue",
"Rule " + rule3.getName() + " - See the single issue", "Rule " + rule3.getName() + " - See the single issue",
"Rule " + rule4.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 .hasParagraph().hasParagraph() // skip footer
.noMoreBlock(); .noMoreBlock();
} }
@Test @Test
public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_analysis_change() { 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"); 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 host = randomAlphabetic(15);
String issueStatusClosed = STATUS_CLOSED; String issueStatusClosed = STATUS_CLOSED;
String otherIssueStatus = STATUS_RESOLVED; String otherIssueStatus = STATUS_RESOLVED;

List<ChangedIssue> changedIssues = Stream.of( List<ChangedIssue> changedIssues = Stream.of(
IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, issueStatusClosed, project1, rule1)), IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, issueStatusClosed, project1, rule1)),
IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, issueStatusClosed, project1, rule2)), IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, issueStatusClosed, project1, rule2)),
IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, otherIssueStatus, project1, rule1))) IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, otherIssueStatus, project1, rule1)))
.flatMap(t -> t) .flatMap(t -> t)
.collect(toList()); .collect(toList());

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


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


@Test @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 project1 = newProject("1");
Project project2 = newProject("V"); Project project2 = newProject("V");
Project project2Branch = newBranch("V", "AB"); 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 status = randomValidStatus();
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of( List<ChangedIssue> changedIssues = Stream.of(
IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, status, project1, rule1)), 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, 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, 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) .flatMap(t -> t)
.collect(toList()); .collect(toList());
Collections.shuffle(changedIssues); Collections.shuffle(changedIssues);
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
UserChange userChange = newUserChange();
when(emailSettings.getServerBaseURL()).thenReturn(host); when(emailSettings.getServerBaseURL()).thenReturn(host);


EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues))); EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
.withLink("See all 40 issues", .withLink("See all 40 issues",
host + "/project/issues?id=" + project1.getKey() host + "/project/issues?id=" + project1.getKey()
+ "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C"))) + "&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()) .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", .withLink("1-40",
host + "/project/issues?id=" + project2.getKey() 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"))) + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
.withLink("81", .withLink("81",
host + "/project/issues?id=" + project2.getKey() host + "/project/issues?id=" + project2.getKey()
+ "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9") + "&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()) .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", .withLink("See all 6 issues",
host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get() host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
+ "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C"))) + "&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 .hasParagraph().hasParagraph() // skip footer
.noMoreBlock(); .noMoreBlock();
} }

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

import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;


public class ChangesOnMyIssuesNotificationTest { public class ChangesOnMyIssuesNotificationTest {
@Test @Test
@Test @Test
public void equals_is_based_on_change_and_issues() { public void equals_is_based_on_change_and_issues() {
AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong()); 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)); ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue));


assertThat(underTest) assertThat(underTest)
@Test @Test
public void hashcode_is_based_on_change_and_issues() { public void hashcode_is_based_on_change_and_issues() {
AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong()); 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)); ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue));


assertThat(underTest.hashCode()) assertThat(underTest.hashCode())

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

import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.sonar.api.issue.Issue; 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.FPOrWontFixNotification.FpOrWontFix;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; 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.User;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
import org.sonar.server.notification.NotificationDispatcherMetadata; import org.sonar.server.notification.NotificationDispatcherMetadata;
import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX; import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
import static org.sonar.core.util.stream.MoreCollectors.index; 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.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.GLOBAL_NOTIFICATION;
import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION; import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER; import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
.setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5))) .setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5)))
.setNewStatus(randomAlphabetic(12)) .setNewStatus(randomAlphabetic(12))
.setNewResolution(randomAlphabetic(13)) .setNewResolution(randomAlphabetic(13))
.setRule(new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), randomAlphabetic(8)))
.setRule(newRandomNotAHotspotRule(randomAlphabetic(8)))
.setProject(new Project.Builder(randomAlphabetic(9)) .setProject(new Project.Builder(randomAlphabetic(9))
.setKey(randomAlphabetic(10)) .setKey(randomAlphabetic(10))
.setProjectName(randomAlphabetic(11)) .setProjectName(randomAlphabetic(11))

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

import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import org.junit.Test; 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.AnalysisChange;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
import static org.assertj.core.api.Assertions.assertThat; 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.FP;
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX; import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX;
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;


public class FPOrWontFixNotificationTest { public class FPOrWontFixNotificationTest {
@Test @Test
public void equals_is_based_on_issues_change_and_resolution() { 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(); 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)) Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> new ChangedIssue.Builder("key_" + i) .mapToObj(i -> new ChangedIssue.Builder("key_" + i)
.setNewStatus("status") .setNewStatus("status")
.setRule(rule) .setRule(rule)
.setProject(project) .setProject(project)
.build())
.build())
.collect(Collectors.toSet()); .collect(Collectors.toSet());
AnalysisChange change = new AnalysisChange(12); AnalysisChange change = new AnalysisChange(12);
User user = new User("uuid", "login", null); User user = new User("uuid", "login", null);
.isNotEqualTo(new FPOrWontFixNotification(new IssuesChangesNotificationBuilder.UserChange(12, user), changedIssues, WONT_FIX)) .isNotEqualTo(new FPOrWontFixNotification(new IssuesChangesNotificationBuilder.UserChange(12, user), changedIssues, WONT_FIX))
.isNotEqualTo(new FPOrWontFixNotification(change, changedIssues, FP)); .isNotEqualTo(new FPOrWontFixNotification(change, changedIssues, FP));
} }

@Test @Test
public void hashcode_is_based_on_issues_change_and_resolution() { 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(); 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)) Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> new ChangedIssue.Builder("key_" + i) .mapToObj(i -> new ChangedIssue.Builder("key_" + i)
.setNewStatus("status") .setNewStatus("status")
.setRule(rule) .setRule(rule)
.setProject(project) .setProject(project)
.build())
.build())
.collect(Collectors.toSet()); .collect(Collectors.toSet());
AnalysisChange change = new AnalysisChange(12); AnalysisChange change = new AnalysisChange(12);
User user = new User("uuid", "login", null); 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

import org.sonar.api.config.EmailSettings; import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification; import org.sonar.api.notifications.Notification;
import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.core.i18n.I18n; import org.sonar.core.i18n.I18n;
import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix; import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix;
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; 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.FP;
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX; 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) @RunWith(DataProviderRunner.class)
public class FpOrWontFixEmailTemplateTest { public class FpOrWontFixEmailTemplateTest {
Project project = newProject("1"); Project project = newProject("1");
String ruleName = randomAlphabetic(8); String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
ChangedIssue changedIssue = newChangedIssue("key", project, ruleName);
ChangedIssue changedIssue = newChangedIssue("key", project, ruleName, randomRuleTypeHotspotExcluded());
when(emailSettings.getServerBaseURL()).thenReturn(host); when(emailSettings.getServerBaseURL()).thenReturn(host);


EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix)); EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
.noMoreBlock(); .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 @Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_single_issue_on_branch(Change change, FpOrWontFix fpOrWontFix) { public void formats_returns_html_message_for_single_issue_on_branch(Change change, FpOrWontFix fpOrWontFix) {
String ruleName = randomAlphabetic(8); String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
String key = "key"; String key = "key";
ChangedIssue changedIssue = newChangedIssue(key, project, ruleName);
ChangedIssue changedIssue = newChangedIssue(key, project, ruleName, randomRuleTypeHotspotExcluded());
when(emailSettings.getServerBaseURL()).thenReturn(host); when(emailSettings.getServerBaseURL()).thenReturn(host);


EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix)); EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
.noMoreBlock(); .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 @Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master(Change change, FpOrWontFix fpOrWontFix) { 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"); Project project = newProject("1");
String ruleName = randomAlphabetic(8); String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
Rule rule = newRule(ruleName);
Rule rule = newRandomNotAHotspotRule(ruleName);
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, project, rule)) .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
.collect(toList()); .collect(toList());
.noMoreBlock(); .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 @Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch(Change change, FpOrWontFix fpOrWontFix) { public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch(Change change, FpOrWontFix fpOrWontFix) {
Project project = newBranch("1", branchName); Project project = newBranch("1", branchName);
String ruleName = randomAlphabetic(8); String ruleName = randomAlphabetic(8);
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
Rule rule = newRule(ruleName);
Rule rule = newRandomNotAHotspotRule(ruleName);
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
.mapToObj(i -> newChangedIssue("issue_" + i, project, rule)) .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
.collect(toList()); .collect(toList());
.noMoreBlock(); .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 @Test
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_with_projects_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) { public void formats_returns_html_message_with_projects_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) {
Project project3 = newProject("C"); Project project3 = newProject("C");
String host = randomAlphabetic(15); String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3) 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()); .collect(toList());
Collections.shuffle(changedIssues); Collections.shuffle(changedIssues);
when(emailSettings.getServerBaseURL()).thenReturn(host); when(emailSettings.getServerBaseURL()).thenReturn(host);
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
public void formats_returns_html_message_with_rules_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) { public void formats_returns_html_message_with_rules_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) {
Project project = newProject("1"); 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 host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4) List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
.map(rule -> newChangedIssue("issue_" + rule.getName(), project, rule)) .map(rule -> newChangedIssue("issue_" + rule.getName(), project, rule))
Project project1 = newProject("1"); Project project1 = newProject("1");
Project project2 = newProject("V"); Project project2 = newProject("V");
Project project2Branch = newBranch("V", "AB"); 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); String host = randomAlphabetic(15);
List<ChangedIssue> changedIssues = Stream.of( List<ChangedIssue> changedIssues = Stream.of(
IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, project1, rule1)), IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, project1, rule1)),
}; };
} }


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) { private static ChangedIssue newChangedIssue(String key, Project project, Rule rule) {
.build(); .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) { private static Project newProject(String uuid) {

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



import java.util.Random; import java.util.Random;
import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.db.DbTester; import org.sonar.db.DbTester;
import org.sonar.db.component.BranchDto; import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentDto;


import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; 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 { public class IssuesChangesNotificationBuilderTesting {


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

private IssuesChangesNotificationBuilderTesting() {
}

public static Rule ruleOf(RuleDto rule) { 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) { 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) { public static User userOf(UserDto changeAuthor) {
.build(); .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) { static ChangedIssue newChangedIssue(String key, String status, Project project, Rule rule) {
.build(); .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) { static Project newProject(String uuid) {
static AnalysisChange newAnalysisChange() { static AnalysisChange newAnalysisChange() {
return new AnalysisChange(new Random().nextLong()); 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

.setNewStatus(issue.status()) .setNewStatus(issue.status())
.setNewResolution(issue.resolution()) .setNewResolution(issue.resolution())
.setAssignee(assignee.map(u -> new User(u.getUuid(), u.getLogin(), u.getName())).orElse(null)) .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()) .setProject(new Project.Builder(projectDto.uuid())
.setKey(projectDto.getKey()) .setKey(projectDto.getKey())
.setProjectName(projectDto.name()) .setProjectName(projectDto.name())

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

import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.RuleStatus; import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rules.RuleType;
import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext; import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.util.stream.MoreCollectors; import org.sonar.core.util.stream.MoreCollectors;
.setNewResolution(issue.resolution()) .setNewResolution(issue.resolution())
.setNewStatus(issue.status()) .setNewStatus(issue.status())
.setAssignee(assignee.map(assigneeDto -> new User(assigneeDto.getUuid(), assigneeDto.getLogin(), assigneeDto.getName())).orElse(null)) .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()) .setProject(new Project.Builder(project.uuid())
.setKey(project.getKey()) .setKey(project.getKey())
.setProjectName(project.name()) .setProjectName(project.name())

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

#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
notification.channel.EmailNotificationChannel=Email notification.channel.EmailNotificationChannel=Email
notification.dispatcher.information=A notification is never sent to the author of the event. 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.NewIssues=New issues
notification.dispatcher.NewAlerts=New quality gate status notification.dispatcher.NewAlerts=New quality gate status
notification.dispatcher.NewFalsePositiveIssue=Issues resolved as false positive or won't fix 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

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

} }

Loading…
Cancel
Save