]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12745 Adjust `changes on my issue` email notification for Security Hotspots
authorJacek <jacek.poreda@sonarsource.com>
Fri, 13 Dec 2019 10:20:52 +0000 (11:20 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:28 +0000 (20:46 +0100)
18 files changed:
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/notification/NotificationFactory.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplate.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/RuleGroup.java [new file with mode: 0644]
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssueNotificationHandlerTest.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesEmailTemplateTest.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/ChangesOnMyIssuesNotificationTest.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationHandlerTest.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrWontFixNotificationTest.java
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FpOrWontFixEmailTemplateTest.java
server/sonar-server-common/src/testFixtures/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilderTesting.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/rules/RuleType.java

index a8af2f653abc19737d027da972ec65261276693f..c66877890cb35d2c1d2dded8fcadd9c56d50ea27 100644 (file)
@@ -111,7 +111,7 @@ public class NotificationFactory {
 
   private IssuesChangesNotificationBuilder.Rule getRuleByRuleKey(RuleKey ruleKey) {
     return ruleRepository.findByKey(ruleKey)
-      .map(t -> new IssuesChangesNotificationBuilder.Rule(ruleKey, t.getName()))
+      .map(t -> new IssuesChangesNotificationBuilder.Rule(ruleKey, t.getType(), t.getName()))
       .orElseThrow(() -> new IllegalStateException("Can not find rule " + ruleKey + " in RuleRepository"));
   }
 
index ab65a1138f10e1f1ccfee2f630e9056a596c8f82..2c34348cc4f229e3b98d822b2b21701cca22b491 100644 (file)
@@ -92,6 +92,7 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
 import static org.sonar.ce.task.projectanalysis.component.Component.Type;
 import static org.sonar.ce.task.projectanalysis.component.ReportComponent.builder;
 import static org.sonar.ce.task.projectanalysis.step.SendIssueNotificationsStep.NOTIF_TYPES;
@@ -133,7 +134,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
   public DbTester db = DbTester.create(System2.INSTANCE);
 
   private final Random random = new Random();
-  private final RuleType[] RULE_TYPES_EXCEPT_HOTSPOTS = Stream.of(RuleType.values()).filter(r -> r != RuleType.SECURITY_HOTSPOT).toArray(RuleType[]::new);
+  private final RuleType[] RULE_TYPES_EXCEPT_HOTSPOTS = Stream.of(RuleType.values()).filter(r -> r != SECURITY_HOTSPOT).toArray(RuleType[]::new);
   private final RuleType randomRuleType = RULE_TYPES_EXCEPT_HOTSPOTS[random.nextInt(RULE_TYPES_EXCEPT_HOTSPOTS.length)];
   @SuppressWarnings("unchecked")
   private Class<Map<String, UserDto>> assigneeCacheType = (Class<Map<String, UserDto>>) (Object) Map.class;
@@ -460,7 +461,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
       .isEmpty();
     Tuple[] expected = stream(users).map(user -> tuple(user.getUuid(), user.getUuid(), user.getId(), user.getLogin())).toArray(Tuple[]::new);
     assertThat(cache.entrySet())
-      .extracting(t -> t.getKey(), t -> t.getValue().getUuid(), t -> t.getValue().getId(), t -> t.getValue().getLogin())
+      .extracting(Map.Entry::getKey, t -> t.getValue().getUuid(), t -> t.getValue().getId(), t -> t.getValue().getLogin())
       .containsOnly(expected);
   }
 
@@ -488,7 +489,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
   }
 
   @Test
-  public void dont_send_issues_change_notification_for_hotspot() {
+  public void do_not_send_new_issues_notifications_for_hotspot() {
     UserDto user = db.users().insertUser();
     ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName());
     ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName());
index a1424caa0b8426fde369b9fac2650021cd1f7c7b..812551a41cce77b8e9dbf7970930127a829f813b 100644 (file)
@@ -21,7 +21,6 @@ package org.sonar.server.issue.notification;
 
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.SetMultimap;
-import java.util.Collection;
 import java.util.List;
 import javax.annotation.CheckForNull;
 import org.sonar.api.config.EmailSettings;
@@ -34,9 +33,13 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
 
 import static com.google.common.base.Preconditions.checkState;
+import static java.util.function.Function.identity;
 import static org.sonar.api.issue.Issue.STATUS_CLOSED;
 import static org.sonar.api.issue.Issue.STATUS_OPEN;
 import static org.sonar.core.util.stream.MoreCollectors.index;
+import static org.sonar.server.issue.notification.RuleGroup.ISSUES;
+import static org.sonar.server.issue.notification.RuleGroup.SECURITY_HOTSPOTS;
+import static org.sonar.server.issue.notification.RuleGroup.resolveGroup;
 
 /**
  * Creates email message for notification "Changes on my issues".
@@ -109,33 +112,28 @@ public class ChangesOnMyIssuesEmailTemplate extends IssueChangesEmailTemplate {
     return new EmailMessage()
       .setFrom(user.getName().orElse(user.getLogin()))
       .setMessageId("changes-on-my-issues")
-      .setSubject("A manual update has changed some of your issues")
+      .setSubject("A manual update has changed some of your issues/hotspots")
       .setHtmlMessage(buildMultiProjectMessage(notification));
   }
 
   private String buildMultiProjectMessage(ChangesOnMyIssuesNotification notification) {
+    ListMultimap<RuleGroup, ChangedIssue> issuesAndHotspots = notification.getChangedIssues().values().stream()
+      .collect(index(changedIssue -> resolveGroup(changedIssue.getRule().getRuleType()), identity()));
+
+    List<ChangedIssue> issues = issuesAndHotspots.get(ISSUES);
+    List<ChangedIssue> hotspots = issuesAndHotspots.get(SECURITY_HOTSPOTS);
+
     StringBuilder sb = new StringBuilder();
     paragraph(sb, s -> s.append("Hi,"));
-    paragraph(sb, s -> {
-      SetMultimap<Project, ChangedIssue> changedIssues = notification.getChangedIssues();
-      s.append("A manual change has updated ").append(issuesOrAnIssue(changedIssues))
-        .append(" assigned to you:");
-    });
+    paragraph(sb, s -> s.append("A manual change has updated ").append(RuleGroup.formatIssuesOrHotspots(issues, hotspots)).append(" assigned to you:"));
 
-    addIssuesByProjectThenRule(sb, notification.getChangedIssues());
+    addIssuesAndHotspotsByProjectThenRule(sb, notification.getChangedIssues());
 
     addFooter(sb, NOTIFICATION_NAME_I18N_KEY);
 
     return sb.toString();
   }
 
-  private static String issueOrIssues(Collection<?> collection) {
-    if (collection.size() > 1) {
-      return "issues";
-    }
-    return "issue";
-  }
-
   private static String issuesOrAnIssue(SetMultimap<Project, ChangedIssue> changedIssues) {
     if (changedIssues.size() > 1) {
       return "issues";
index 4774ba5663f7da1fdec20dfbfd1073b13452adf9..53d4f8650e5abbd5408df3858edd2feb7f4ed637 100644 (file)
@@ -31,10 +31,13 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Optional;
+import java.util.Set;
 import java.util.SortedSet;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import javax.annotation.Nullable;
 import org.sonar.api.config.EmailSettings;
+import org.sonar.api.rules.RuleType;
 import org.sonar.core.i18n.I18n;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
@@ -42,7 +45,13 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule
 
 import static java.net.URLEncoder.encode;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.function.Function.identity;
 import static org.sonar.core.util.stream.MoreCollectors.index;
+import static org.sonar.server.issue.notification.RuleGroup.ISSUES;
+import static org.sonar.server.issue.notification.RuleGroup.SECURITY_HOTSPOTS;
+import static org.sonar.server.issue.notification.RuleGroup.formatIssueOrHotspot;
+import static org.sonar.server.issue.notification.RuleGroup.formatIssuesOrHotspots;
+import static org.sonar.server.issue.notification.RuleGroup.resolveGroup;
 
 public abstract class IssueChangesEmailTemplate implements EmailTemplate {
 
@@ -50,6 +59,7 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
   private static final Comparator<Project> PROJECT_COMPARATOR = Comparator.comparing(Project::getProjectName)
     .thenComparing(t -> t.getBranchName().orElse(""));
   private static final Comparator<ChangedIssue> CHANGED_ISSUE_KEY_COMPARATOR = Comparator.comparing(ChangedIssue::getKey, Comparator.naturalOrder());
+
   /**
    * Assuming:
    * <ul>
@@ -97,6 +107,38 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
       });
   }
 
+  void addIssuesAndHotspotsByProjectThenRule(StringBuilder sb, SetMultimap<Project, ChangedIssue> issuesByProject) {
+    issuesByProject.keySet().stream()
+      .sorted(PROJECT_COMPARATOR)
+      .forEach(project -> {
+        String encodedProjectParams = toUrlParams(project);
+        paragraph(sb, s -> toString(s, project));
+
+        Set<ChangedIssue> changedIssues = issuesByProject.get(project);
+        ListMultimap<RuleGroup, ChangedIssue> issuesAndHotspots = changedIssues.stream()
+          .collect(index(changedIssue -> resolveGroup(changedIssue.getRule().getRuleType()), identity()));
+
+        List<ChangedIssue> issues = issuesAndHotspots.get(ISSUES);
+        List<ChangedIssue> hotspots = issuesAndHotspots.get(SECURITY_HOTSPOTS);
+
+        boolean hasSecurityHotspots = !hotspots.isEmpty();
+        boolean hasOtherIssues = !issues.isEmpty();
+
+        if (hasOtherIssues) {
+          addIssuesByRule(sb, issues, projectIssuePageHref(encodedProjectParams));
+        }
+
+        if (hasSecurityHotspots && hasOtherIssues) {
+          paragraph(sb, stringBuilder -> {
+          });
+        }
+
+        if (hasSecurityHotspots) {
+          addIssuesByRule(sb, hotspots, securityHotspotPageHref(encodedProjectParams));
+        }
+      });
+  }
+
   void addIssuesByRule(StringBuilder sb, Collection<ChangedIssue> changedIssues, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref) {
     ListMultimap<Rule, ChangedIssue> issuesByRule = changedIssues.stream()
       .collect(index(ChangedIssue::getRule, t -> t));
@@ -114,21 +156,22 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
       Collection<ChangedIssue> issues = issuesByRule.get(rule);
 
       sb.append("<li>").append("Rule ").append(" <em>").append(rule.getName()).append("</em> - ");
-      appendIssueLinks(sb, issuePageHref, issues);
+      appendIssueLinks(sb, issuePageHref, issues, rule.getRuleType());
       sb.append("</li>");
     }
     sb.append("</ul>");
   }
 
-  private static void appendIssueLinks(StringBuilder sb, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref, Collection<ChangedIssue> issues) {
+  private static void appendIssueLinks(StringBuilder sb, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref, Collection<ChangedIssue> issues,
+    @Nullable RuleType ruleType) {
     SortedSet<ChangedIssue> sortedIssues = ImmutableSortedSet.copyOf(CHANGED_ISSUE_KEY_COMPARATOR, issues);
     int issueCount = issues.size();
     if (issueCount == 1) {
-      link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See the single issue"));
+      link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See the single ").append(formatIssueOrHotspot(ruleType)));
     } else if (issueCount <= MAX_ISSUES_BY_LINK) {
-      link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See all ").append(issueCount).append(" issues"));
+      link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See all ").append(issueCount).append(" ").append(formatIssuesOrHotspots(ruleType)));
     } else {
-      sb.append("See issues");
+      sb.append("See ").append(formatIssuesOrHotspots(ruleType));
       List<List<ChangedIssue>> issueGroups = Lists.partition(ImmutableList.copyOf(sortedIssues), MAX_ISSUES_BY_LINK);
       Iterator<List<ChangedIssue>> issueGroupsIterator = issueGroups.iterator();
       int[] groupIndex = new int[] {0};
@@ -160,6 +203,21 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
     };
   }
 
+  BiConsumer<StringBuilder, Collection<ChangedIssue>> securityHotspotPageHref(String projectParams) {
+    return (s, issues) -> {
+      s.append(settings.getServerBaseURL()).append("/security_hotspots?").append(projectParams)
+        .append("&hotspots=");
+
+      Iterator<ChangedIssue> issueIterator = issues.iterator();
+      while (issueIterator.hasNext()) {
+        s.append(urlEncode(issueIterator.next().getKey()));
+        if (issueIterator.hasNext()) {
+          s.append(URL_ENCODED_COMMA);
+        }
+      }
+    };
+  }
+
   private static Consumer<StringBuilder> issueGroupLabel(StringBuilder sb, int[] groupIndex, List<ChangedIssue> issueGroup) {
     return s -> {
       int firstIssueNumber = (groupIndex[0] * MAX_ISSUES_BY_LINK) + 1;
@@ -207,4 +265,11 @@ public abstract class IssueChangesEmailTemplate implements EmailTemplate {
     }
   }
 
+  protected static String issueOrIssues(Collection<?> collection) {
+    if (collection.size() > 1) {
+      return "issues";
+    }
+    return "issue";
+  }
+
 }
index c1508db355cacf3f921831650410f19a9fafa746..5779d4989b34f5f574bf8878939f42aafc46c749 100644 (file)
@@ -27,6 +27,7 @@ import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
@@ -233,10 +234,16 @@ public class IssuesChangesNotificationBuilder {
   @Immutable
   public static final class Rule {
     private final RuleKey key;
+    private final RuleType ruleType;
     private final String name;
 
-    public Rule(RuleKey key, String name) {
+    public Rule(RuleKey key, @Nullable String ruleType, String name) {
+      this(key, ruleType != null ? RuleType.valueOf(ruleType) : null, name);
+    }
+
+    public Rule(RuleKey key, @Nullable RuleType ruleType, String name) {
       this.key = requireNonNull(key, KEY_CANT_BE_NULL_MESSAGE);
+      this.ruleType = ruleType;
       this.name = requireNonNull(name, "name can't be null");
     }
 
@@ -244,6 +251,11 @@ public class IssuesChangesNotificationBuilder {
       return key;
     }
 
+    @CheckForNull
+    public RuleType getRuleType() {
+      return ruleType;
+    }
+
     public String getName() {
       return name;
     }
@@ -257,18 +269,19 @@ public class IssuesChangesNotificationBuilder {
         return false;
       }
       Rule that = (Rule) o;
-      return key.equals(that.key) && name.equals(that.name);
+      return key.equals(that.key) && ruleType.equals(that.ruleType) && name.equals(that.name);
     }
 
     @Override
     public int hashCode() {
-      return Objects.hash(key, name);
+      return Objects.hash(key, ruleType, name);
     }
 
     @Override
     public String toString() {
       return "Rule{" +
         "key=" + key +
+        ", type=" + ruleType +
         ", name='" + name + '\'' +
         '}';
     }
index 1c5c652e8e030ea2a0e82495465134f171631bbf..4b172445ab111f15903926825b6bb2d8a22e2aef 100644 (file)
@@ -35,6 +35,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static java.util.Optional.ofNullable;
 import static org.sonar.core.util.stream.MoreCollectors.toSet;
 import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
 
@@ -44,6 +45,7 @@ public class IssuesChangesNotificationSerializer {
   private static final String FIELD_CHANGE_AUTHOR_UUID = "change.author.uuid";
   private static final String FIELD_CHANGE_AUTHOR_LOGIN = "change.author.login";
   private static final String FIELD_CHANGE_AUTHOR_NAME = "change.author.name";
+  private static final String FIELD_PREFIX_RULES = "rules.";
 
   public IssuesChangesNotification serialize(IssuesChangesNotificationBuilder builder) {
     IssuesChangesNotification res = new IssuesChangesNotification();
@@ -160,7 +162,10 @@ public class IssuesChangesNotificationSerializer {
     issues.stream()
       .map(ChangedIssue::getRule)
       .collect(Collectors.toSet())
-      .forEach(rule -> res.setFieldValue("rules." + rule.getKey(), rule.getName()));
+      .forEach(rule -> {
+        res.setFieldValue(FIELD_PREFIX_RULES + rule.getKey(), rule.getName());
+        ofNullable(rule.getRuleType()).ifPresent(ruleType -> res.setFieldValue(FIELD_PREFIX_RULES + rule.getKey() + ".type", rule.getRuleType().name()));
+      });
   }
 
   private static Map<RuleKey, Rule> readRules(IssuesChangesNotification notification, List<Issue> issues) {
@@ -173,10 +178,11 @@ public class IssuesChangesNotificationSerializer {
   }
 
   private static Rule readRule(IssuesChangesNotification notification, RuleKey ruleKey) {
-    String fieldName = "rules." + ruleKey;
+    String fieldName = FIELD_PREFIX_RULES + ruleKey;
     String ruleName = notification.getFieldValue(fieldName);
+    String ruleType = notification.getFieldValue(fieldName + ".type");
     checkState(ruleName != null, "can not find field %s", ruleKey);
-    return new Rule(ruleKey, ruleName);
+    return new Rule(ruleKey, ruleType, ruleName);
   }
 
   private static void serializeProjects(IssuesChangesNotification res, Set<ChangedIssue> issues) {
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/RuleGroup.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/RuleGroup.java
new file mode 100644 (file)
index 0000000..2bce7b3
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.issue.notification;
+
+import java.util.Collection;
+import javax.annotation.Nullable;
+import org.sonar.api.rules.RuleType;
+
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
+
+enum RuleGroup {
+  SECURITY_HOTSPOTS,
+  ISSUES;
+
+  static RuleGroup resolveGroup(@Nullable RuleType ruleType) {
+    return SECURITY_HOTSPOT.equals(ruleType) ? SECURITY_HOTSPOTS : ISSUES;
+  }
+
+  static String formatIssuesOrHotspots(Collection<?> issues, Collection<?> hotspots) {
+    if (!issues.isEmpty() && !hotspots.isEmpty()) {
+      return "issues/hotspots";
+    }
+    if (issues.size() == 1) {
+      return "an issue";
+    }
+    if (issues.size() > 1) {
+      return "issues";
+    }
+    if (hotspots.size() == 1) {
+      return "a hotspot";
+    }
+    return "hotspots";
+  }
+
+  static String formatIssueOrHotspot(@Nullable RuleType ruleType) {
+    if (SECURITY_HOTSPOT.equals(ruleType)) {
+      return "hotspot";
+    }
+    return "issue";
+  }
+
+  static String formatIssuesOrHotspots(@Nullable RuleType ruleType) {
+    if (SECURITY_HOTSPOT.equals(ruleType)) {
+      return "hotspots";
+    }
+    return "issues";
+  }
+}
index da9a10978fd0ab58acebe3e4daae9d44742e526e..e18f8b3be9ecabaccf87d342b65d9106b6c098aa 100644 (file)
@@ -36,7 +36,6 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
-import org.sonar.api.rule.RuleKey;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
@@ -61,6 +60,7 @@ import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 import static org.sonar.core.util.stream.MoreCollectors.index;
 import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
 import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
 import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
 import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
@@ -354,10 +354,10 @@ public class ChangesOnMyIssueNotificationHandlerTest {
       .collect(toSet());
     when(notificationManager.findSubscribedEmailRecipients(
       CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project1.getKey(), ImmutableSet.of(assignee1.getLogin()), ALL_MUST_HAVE_ROLE_USER))
-      .thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin())));
+        .thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin())));
     when(notificationManager.findSubscribedEmailRecipients(
       CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project2.getKey(), ImmutableSet.of(assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER))
-      .thenReturn(ImmutableSet.of(emailRecipientOf(assignee2.getLogin())));
+        .thenReturn(ImmutableSet.of(emailRecipientOf(assignee2.getLogin())));
     int deliveredCount = new Random().nextInt(100);
     when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount);
 
@@ -437,7 +437,7 @@ public class ChangesOnMyIssueNotificationHandlerTest {
   }
 
   private static Rule newRule() {
-    return new Rule(RuleKey.of(randomAlphabetic(3), randomAlphabetic(4)), randomAlphabetic(5));
+    return newRandomNotAHotspotRule(randomAlphabetic(5));
   }
 
   private static Set<IssuesChangesNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) {
index 6017143a11c84e8779856d5f05ce616cf90cdf1f..ad7343ccceddfb84d9f0460c6d5d81d2f6370db2 100644 (file)
@@ -32,11 +32,13 @@ import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
+import org.elasticsearch.common.util.set.Sets;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.sonar.api.config.EmailSettings;
 import org.sonar.api.notifications.Notification;
+import org.sonar.api.rules.RuleType;
 import org.sonar.core.i18n.I18n;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
@@ -60,14 +62,24 @@ import static org.sonar.api.issue.Issue.STATUS_CONFIRMED;
 import static org.sonar.api.issue.Issue.STATUS_OPEN;
 import static org.sonar.api.issue.Issue.STATUS_REOPENED;
 import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
+import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
+import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newAnalysisChange;
 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newBranch;
 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newChangedIssue;
 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRule;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newSecurityHotspotRule;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newUserChange;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.randomRuleTypeHotspotExcluded;
 
 @RunWith(DataProviderRunner.class)
 public class ChangesOnMyIssuesEmailTemplateTest {
   private static final String[] ISSUE_STATUSES = {STATUS_OPEN, STATUS_RESOLVED, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_CLOSED};
+  private static final String[] SECURITY_HOTSPOTS_STATUSES = {STATUS_TO_REVIEW, STATUS_REVIEWED};
+
   @org.junit.Rule
   public ExpectedException expectedException = ExpectedException.none();
 
@@ -84,7 +96,7 @@ public class ChangesOnMyIssuesEmailTemplateTest {
 
   @Test
   public void formats_fails_with_ISE_if_change_from_Analysis_and_no_issue() {
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
 
     expectedException.expect(IllegalStateException.class);
     expectedException.expectMessage("changedIssues can't be empty");
@@ -95,9 +107,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   @Test
   public void format_sets_message_id_with_project_key_of_first_issue_in_set_when_change_from_Analysis() {
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
-      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
       .collect(toSet());
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
 
@@ -107,7 +119,7 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   @Test
   public void format_sets_subject_with_project_name_of_first_issue_in_set_when_change_from_Analysis() {
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
-      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
       .collect(toSet());
     AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
 
@@ -120,9 +132,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   @Test
   public void format_sets_subject_with_project_name_and_branch_name_of_first_issue_in_set_when_change_from_Analysis() {
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
-      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newBranch("prj_" + i, "br_" + i), newRule("rule_" + i)))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newBranch("prj_" + i, "br_" + i), newRandomNotAHotspotRule("rule_" + i)))
       .collect(toSet());
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
 
@@ -133,9 +145,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   @Test
   public void format_set_html_message_with_header_dealing_with_plural_when_change_from_Analysis() {
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
-      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
       .collect(toSet());
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
 
     EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues.stream().limit(1).collect(toSet())));
     EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
@@ -151,9 +163,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   @Test
   public void format_sets_static_message_id_when_change_from_User() {
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
-      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
       .collect(toSet());
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    UserChange userChange = newUserChange();
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
 
@@ -163,21 +175,21 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   @Test
   public void format_sets_static_subject_when_change_from_User() {
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
-      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
       .collect(toSet());
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    UserChange userChange = newUserChange();
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
 
-    assertThat(emailMessage.getSubject()).isEqualTo("A manual update has changed some of your issues");
+    assertThat(emailMessage.getSubject()).isEqualTo("A manual update has changed some of your issues/hotspots");
   }
 
   @Test
-  public void format_set_html_message_with_header_dealing_with_plural_when_change_from_User() {
+  public void format_set_html_message_with_header_dealing_with_plural_issues_when_change_from_User() {
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
-      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i)))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
       .collect(toSet());
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    UserChange userChange = newUserChange();
 
     EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
       userChange, changedIssues.stream().limit(1).collect(toSet())));
@@ -195,26 +207,91 @@ public class ChangesOnMyIssuesEmailTemplateTest {
       .withoutLink();
   }
 
+  @Test
+  public void format_set_html_message_with_header_dealing_with_plural_security_hotspots_when_change_from_User() {
+    Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newSecurityHotspotRule("rule_" + i)))
+      .collect(toSet());
+    UserChange userChange = newUserChange();
+
+    EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(
+      userChange, changedIssues.stream().limit(1).collect(toSet())));
+    EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues));
+
+    HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage())
+      .hasParagraph("Hi,")
+      .withoutLink()
+      .hasParagraph("A manual change has updated a hotspot assigned to you:")
+      .withoutLink();
+    HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
+      .hasParagraph("Hi,")
+      .withoutLink()
+      .hasParagraph("A manual change has updated hotspots assigned to you:")
+      .withoutLink();
+  }
+
+  @Test
+  public void format_set_html_message_with_header_dealing_with_plural_security_hotspots_and_issues_when_change_from_User() {
+    Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRandomNotAHotspotRule("rule_" + i)))
+      .collect(toSet());
+
+    Set<ChangedIssue> changedHotspots = IntStream.range(0, 2 + new Random().nextInt(4))
+      .mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newSecurityHotspotRule("rule_" + i)))
+      .collect(toSet());
+
+    Set<ChangedIssue> issuesAndHotspots = Sets.union(changedIssues, changedHotspots);
+
+    UserChange userChange = newUserChange();
+
+    EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, issuesAndHotspots));
+
+    HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage())
+      .hasParagraph("Hi,")
+      .withoutLink()
+      .hasParagraph("A manual change has updated issues/hotspots assigned to you:")
+      .withoutLink();
+  }
+
   @Test
   @UseDataProvider("issueStatuses")
-  public void format_set_html_message_with_footer_when_change_from_user(String issueStatus) {
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+  public void format_set_html_message_with_footer_when_issue_change_from_user(String issueStatus) {
+    UserChange userChange = newUserChange();
     format_set_html_message_with_footer(userChange, issueStatus, c -> c
       // skip content
-      .hasParagraph() // open/closed issue
-      .hasList() // rule list
-    );
+      .hasParagraph() // skip project header
+      .hasList(), // rule list,
+      randomRuleTypeHotspotExcluded());
   }
 
   @Test
   @UseDataProvider("issueStatuses")
-  public void format_set_html_message_with_footer_when_change_from_analysis(String issueStatus) {
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+  public void format_set_html_message_with_footer_when_issue_change_from_analysis(String issueStatus) {
+    AnalysisChange analysisChange = newAnalysisChange();
     format_set_html_message_with_footer(analysisChange, issueStatus, c -> c
-      // skip content
       .hasParagraph() // status
-      .hasList() // rule list
-    );
+      .hasList(), // rule list,
+      randomRuleTypeHotspotExcluded());
+  }
+
+  @Test
+  @UseDataProvider("securityHotspotsStatuses")
+  public void format_set_html_message_with_footer_when_security_hotspot_change_from_analysis(String securityHotspotStatus) {
+    AnalysisChange analysisChange = newAnalysisChange();
+    format_set_html_message_with_footer(analysisChange, securityHotspotStatus, c -> c
+      .hasParagraph()
+      .hasList(), // rule list
+      SECURITY_HOTSPOT);
+  }
+
+  @Test
+  @UseDataProvider("securityHotspotsStatuses")
+  public void format_set_html_message_with_footer_when_security_hotspot_change_from_user(String securityHotspotStatus) {
+    UserChange userChange = newUserChange();
+    format_set_html_message_with_footer(userChange, securityHotspotStatus, c -> c
+      .hasParagraph()
+      .hasList(), // rule list
+      SECURITY_HOTSPOT);
   }
 
   @DataProvider
@@ -224,14 +301,21 @@ public class ChangesOnMyIssuesEmailTemplateTest {
       .toArray(Object[][]::new);
   }
 
-  private void format_set_html_message_with_footer(Change change, String issueStatus, Function<HtmlParagraphAssert, HtmlListAssert> skipContent) {
+  @DataProvider
+  public static Object[][] securityHotspotsStatuses() {
+    return Arrays.stream(SECURITY_HOTSPOTS_STATUSES)
+      .map(t -> new Object[] {t})
+      .toArray(Object[][]::new);
+  }
+
+  private void format_set_html_message_with_footer(Change change, String issueStatus, Function<HtmlParagraphAssert, HtmlListAssert> skipContent, RuleType ruleType) {
     String wordingNotification = randomAlphabetic(20);
     String host = randomAlphabetic(15);
     when(i18n.message(Locale.ENGLISH, "notification.dispatcher.ChangesOnMyIssue", "notification.dispatcher.ChangesOnMyIssue"))
       .thenReturn(wordingNotification);
     when(emailSettings.getServerBaseURL()).thenReturn(host);
     Project project = newProject("foo");
-    Rule rule = newRule("bar");
+    Rule rule = newRule("bar", ruleType);
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4))
       .mapToObj(i -> newChangedIssue(i + "", issueStatus, project, rule))
       .collect(toSet());
@@ -260,11 +344,11 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   @Test
   public void format_set_html_message_with_issues_grouped_by_status_closed_or_any_other_when_change_from_analysis() {
     Project project = newProject("foo");
-    Rule rule = newRule("bar");
+    Rule rule = newRandomNotAHotspotRule("bar");
     Set<ChangedIssue> changedIssues = Arrays.stream(ISSUE_STATUSES)
       .map(status -> newChangedIssue(status + "", status, project, rule))
       .collect(toSet());
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues));
 
@@ -282,16 +366,16 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   }
 
   @Test
-  public void format_set_html_message_with_status_title_handles_plural_when_change_from_analysis() {
+  public void format_set_html_message_with_issue_status_title_handles_plural_when_change_from_analysis() {
     Project project = newProject("foo");
-    Rule rule = newRule("bar");
+    Rule rule = newRandomNotAHotspotRule("bar");
     Set<ChangedIssue> closedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(status -> newChangedIssue(status + "", STATUS_CLOSED, project, rule))
       .collect(toSet());
     Set<ChangedIssue> openIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(status -> newChangedIssue(status + "", STATUS_OPEN, project, rule))
       .collect(toSet());
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
 
     EmailMessage closedIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, closedIssues));
     EmailMessage openIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, openIssues));
@@ -313,8 +397,8 @@ public class ChangesOnMyIssuesEmailTemplateTest {
     Project project = newProject("1");
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
-    ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName);
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
+    AnalysisChange analysisChange = newAnalysisChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
@@ -333,8 +417,8 @@ public class ChangesOnMyIssuesEmailTemplateTest {
     Project project = newProject("1");
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
-    ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName);
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
+    UserChange userChange = newUserChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
@@ -355,8 +439,8 @@ public class ChangesOnMyIssuesEmailTemplateTest {
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
     String key = "key";
-    ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName);
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
+    AnalysisChange analysisChange = newAnalysisChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)));
@@ -378,8 +462,8 @@ public class ChangesOnMyIssuesEmailTemplateTest {
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
     String key = "key";
-    ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName);
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName, randomRuleTypeHotspotExcluded());
+    UserChange userChange = newUserChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue)));
@@ -399,12 +483,12 @@ public class ChangesOnMyIssuesEmailTemplateTest {
     Project project = newProject("1");
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
-    Rule rule = newRule(ruleName);
+    Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
     String issueStatus = randomValidStatus();
     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(i -> newChangedIssue("issue_" + i, issueStatus, project, rule))
       .collect(toList());
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
@@ -426,11 +510,11 @@ public class ChangesOnMyIssuesEmailTemplateTest {
     Project project = newProject("1");
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
-    Rule rule = newRule(ruleName);
+    Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
       .collect(toList());
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    UserChange userChange = newUserChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
@@ -453,12 +537,12 @@ public class ChangesOnMyIssuesEmailTemplateTest {
     Project project = newBranch("1", branchName);
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
-    Rule rule = newRule(ruleName);
+    Rule rule = newRule(ruleName, randomRuleTypeHotspotExcluded());
     String status = randomValidStatus();
     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(i -> newChangedIssue("issue_" + i, status, project, rule))
       .collect(toList());
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
@@ -481,11 +565,11 @@ public class ChangesOnMyIssuesEmailTemplateTest {
     Project project = newBranch("1", branchName);
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
-    Rule rule = newRule(ruleName);
+    Rule rule = newRandomNotAHotspotRule(ruleName);
     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule))
       .collect(toList());
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    UserChange userChange = newUserChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
@@ -512,10 +596,10 @@ public class ChangesOnMyIssuesEmailTemplateTest {
     Project project3 = newProject("C");
     String host = randomAlphabetic(15);
     List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3)
-      .map(project -> newChangedIssue("issue_" + project.getUuid(), randomValidStatus(), project, newRule(randomAlphabetic(2))))
+      .map(project -> newChangedIssue("issue_" + project.getUuid(), randomValidStatus(), project, newRule(randomAlphabetic(2), randomRuleTypeHotspotExcluded())))
       .collect(toList());
     Collections.shuffle(changedIssues);
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    UserChange userChange = newUserChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
@@ -541,17 +625,18 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   @Test
   public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change() {
     Project project = newProject("1");
-    Rule rule1 = newRule("1");
-    Rule rule2 = newRule("a");
-    Rule rule3 = newRule("b");
-    Rule rule4 = newRule("X");
+    Rule rule1 = newRandomNotAHotspotRule("1");
+    Rule rule2 = newRandomNotAHotspotRule("a");
+    Rule rule3 = newRandomNotAHotspotRule("b");
+    Rule rule4 = newRandomNotAHotspotRule("X");
+
     String host = randomAlphabetic(15);
     String issueStatus = randomValidStatus();
     List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
       .map(rule -> newChangedIssue("issue_" + rule.getName(), issueStatus, project, rule))
       .collect(toList());
     Collections.shuffle(changedIssues);
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
@@ -569,30 +654,43 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   }
 
   @Test
-  public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change_when_user_analysis() {
+  public void formats_returns_html_message_with_rules_ordered_by_name_user_change() {
     Project project = newProject("1");
-    Rule rule1 = newRule("1");
-    Rule rule2 = newRule("a");
-    Rule rule3 = newRule("b");
-    Rule rule4 = newRule("X");
+    Rule rule1 = newRandomNotAHotspotRule("1");
+    Rule rule2 = newRandomNotAHotspotRule("a");
+    Rule rule3 = newRandomNotAHotspotRule("b");
+    Rule rule4 = newRandomNotAHotspotRule("X");
+
+    Rule hotspot1 = newSecurityHotspotRule("S");
+    Rule hotspot2 = newSecurityHotspotRule("Z");
+    Rule hotspot3 = newSecurityHotspotRule("N");
+    Rule hotspot4 = newSecurityHotspotRule("M");
+
     String host = randomAlphabetic(15);
-    List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
+    List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4, hotspot1, hotspot2, hotspot3, hotspot4)
       .map(rule -> newChangedIssue("issue_" + rule.getName(), randomValidStatus(), project, rule))
       .collect(toList());
     Collections.shuffle(changedIssues);
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    UserChange userChange = newUserChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
 
     HtmlFragmentAssert.assertThat(emailMessage.getMessage())
-      .hasParagraph().hasParagraph() // skip header
-      .hasParagraph(project.getProjectName())
+      .hasParagraph()
+      .hasParagraph()
+      .hasParagraph() // skip project name
       .hasList(
         "Rule " + rule1.getName() + " - See the single issue",
         "Rule " + rule2.getName() + " - See the single issue",
         "Rule " + rule3.getName() + " - See the single issue",
         "Rule " + rule4.getName() + " - See the single issue")
+      .hasEmptyParagraph()
+      .hasList(
+        "Rule " + hotspot1.getName() + " - See the single hotspot",
+        "Rule " + hotspot2.getName() + " - See the single hotspot",
+        "Rule " + hotspot3.getName() + " - See the single hotspot",
+        "Rule " + hotspot4.getName() + " - See the single hotspot")
       .hasParagraph().hasParagraph() // skip footer
       .noMoreBlock();
   }
@@ -600,11 +698,13 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   @Test
   public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_analysis_change() {
     Project project1 = newProject("1");
-    Rule rule1 = newRule("1");
-    Rule rule2 = newRule("a");
+    Rule rule1 = newRandomNotAHotspotRule("1");
+    Rule rule2 = newRandomNotAHotspotRule("a");
+
     String host = randomAlphabetic(15);
     String issueStatusClosed = STATUS_CLOSED;
     String otherIssueStatus = STATUS_RESOLVED;
+
     List<ChangedIssue> changedIssues = Stream.of(
       IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, issueStatusClosed, project1, rule1)),
       IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, issueStatusClosed, project1, rule2)),
@@ -612,8 +712,9 @@ public class ChangesOnMyIssuesEmailTemplateTest {
       IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, otherIssueStatus, project1, rule1)))
       .flatMap(t -> t)
       .collect(toList());
+
     Collections.shuffle(changedIssues);
-    AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange();
+    AnalysisChange analysisChange = newAnalysisChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues)));
@@ -651,23 +752,32 @@ public class ChangesOnMyIssuesEmailTemplateTest {
   }
 
   @Test
-  public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_user_change() {
+  public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_and_hotspots_when_user_change() {
     Project project1 = newProject("1");
     Project project2 = newProject("V");
     Project project2Branch = newBranch("V", "AB");
-    Rule rule1 = newRule("1");
-    Rule rule2 = newRule("a");
+    Rule rule1 = newRule("1", randomRuleTypeHotspotExcluded());
+    Rule rule2 = newRule("a", randomRuleTypeHotspotExcluded());
+
+    Rule hotspot1 = newSecurityHotspotRule("h1");
+    Rule hotspot2 = newSecurityHotspotRule("h2");
+
     String status = randomValidStatus();
     String host = randomAlphabetic(15);
     List<ChangedIssue> changedIssues = Stream.of(
       IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, status, project1, rule1)),
       IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, status, project1, rule2)),
       IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, status, project2, rule2)),
-      IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, status, project2Branch, rule1)))
+      IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, status, project2Branch, rule1)),
+
+      IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, STATUS_REVIEWED, project1, hotspot1)),
+      IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, STATUS_REVIEWED, project1, hotspot2)),
+      IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, STATUS_TO_REVIEW, project2, hotspot2)),
+      IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, STATUS_TO_REVIEW, project2Branch, hotspot1)))
       .flatMap(t -> t)
       .collect(toList());
     Collections.shuffle(changedIssues);
-    UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange();
+    UserChange userChange = newUserChange();
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues)));
@@ -685,8 +795,20 @@ public class ChangesOnMyIssuesEmailTemplateTest {
       .withLink("See all 40 issues",
         host + "/project/issues?id=" + project1.getKey()
           + "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
+      .hasEmptyParagraph()
+      .hasList()
+      .withItemTexts(
+        "Rule " + hotspot1.getName() + " - See all 39 hotspots",
+        "Rule " + hotspot2.getName() + " - See all 40 hotspots")
+      .withLink("See all 39 hotspots",
+        host + "/security_hotspots?id=" + project1.getKey()
+          + "&hotspots=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C")))
+      .withLink("See all 40 hotspots",
+        host + "/security_hotspots?id=" + project1.getKey()
+          + "&hotspots=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C")))
       .hasParagraph(project2.getProjectName())
-      .hasList("Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
+      .hasList(
+        "Rule " + rule2.getName() + " - See issues 1-40 41-80 81")
       .withLink("1-40",
         host + "/project/issues?id=" + project2.getKey()
           + "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
@@ -696,11 +818,28 @@ public class ChangesOnMyIssuesEmailTemplateTest {
       .withLink("81",
         host + "/project/issues?id=" + project2.getKey()
           + "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9")
+      .hasEmptyParagraph()
+      .hasList("Rule " + hotspot2.getName() + " - See hotspots 1-40 41-80 81")
+      .withLink("1-40",
+        host + "/security_hotspots?id=" + project2.getKey()
+          + "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C")))
+      .withLink("41-80",
+        host + "/security_hotspots?id=" + project2.getKey()
+          + "&hotspots=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C")))
+      .withLink("81",
+        host + "/security_hotspots?id=" + project2.getKey()
+          + "&hotspots=" + "1-40_41-80_1_9")
       .hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get())
-      .hasList("Rule " + rule1.getName() + " - See all 6 issues")
+      .hasList(
+        "Rule " + rule1.getName() + " - See all 6 issues")
       .withLink("See all 6 issues",
         host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
           + "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
+      .hasEmptyParagraph()
+      .hasList("Rule " + hotspot1.getName() + " - See all 6 hotspots")
+      .withLink("See all 6 hotspots",
+        host + "/security_hotspots?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get()
+          + "&hotspots=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C")))
       .hasParagraph().hasParagraph() // skip footer
       .noMoreBlock();
   }
index 35dbe097585c302729b5869093475444b8c4861f..5bce5adc5a8bfc42a98170632f05f03caf1120df 100644 (file)
@@ -31,6 +31,7 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
 
 public class ChangesOnMyIssuesNotificationTest {
   @Test
@@ -45,7 +46,8 @@ public class ChangesOnMyIssuesNotificationTest {
   @Test
   public void equals_is_based_on_change_and_issues() {
     AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
-    ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"), IssuesChangesNotificationBuilderTesting.newRule("rul"));
+    ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"),
+      newRandomNotAHotspotRule("rul"));
     ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue));
 
     assertThat(underTest)
@@ -59,7 +61,8 @@ public class ChangesOnMyIssuesNotificationTest {
   @Test
   public void hashcode_is_based_on_change_and_issues() {
     AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong());
-    ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"), IssuesChangesNotificationBuilderTesting.newRule("rul"));
+    ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"),
+      newRandomNotAHotspotRule("rul"));
     ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue));
 
     assertThat(underTest.hashCode())
index 67ec465b4efb8e1329fe97e5ecc742b3fc521314..b0d74a7fdaafdb224e358f14368438bd203fbf8d 100644 (file)
@@ -34,12 +34,10 @@ import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 import org.sonar.api.issue.Issue;
-import org.sonar.api.rule.RuleKey;
 import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
-import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
 import org.sonar.server.notification.NotificationDispatcherMetadata;
@@ -66,6 +64,7 @@ import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
 import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
 import static org.sonar.core.util.stream.MoreCollectors.index;
 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
 import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
 import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
 import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
@@ -477,7 +476,7 @@ public class FPOrWontFixNotificationHandlerTest {
           .setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5)))
           .setNewStatus(randomAlphabetic(12))
           .setNewResolution(randomAlphabetic(13))
-          .setRule(new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), randomAlphabetic(8)))
+          .setRule(newRandomNotAHotspotRule(randomAlphabetic(8)))
           .setProject(new Project.Builder(randomAlphabetic(9))
             .setKey(randomAlphabetic(10))
             .setProjectName(randomAlphabetic(11))
index ed1e5e59e5c3ac7b85aeae8d8ffaec847e432d5b..831745248081b299c6fb93573c5a7c889eefd7a7 100644 (file)
@@ -26,7 +26,6 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import org.junit.Test;
-import org.sonar.api.rule.RuleKey;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
@@ -36,18 +35,19 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP;
 import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
 
 public class FPOrWontFixNotificationTest {
   @Test
   public void equals_is_based_on_issues_change_and_resolution() {
-    Rule rule = new Rule(RuleKey.of("repo", "rule_key"), "rule_name");
+    Rule rule = newRandomNotAHotspotRule("rule_name");
     Project project = new Project.Builder("prj_uuid").setKey("prj_key").setProjectName("prj_name").build();
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(i -> new ChangedIssue.Builder("key_" + i)
         .setNewStatus("status")
         .setRule(rule)
         .setProject(project)
-      .build())
+        .build())
       .collect(Collectors.toSet());
     AnalysisChange change = new AnalysisChange(12);
     User user = new User("uuid", "login", null);
@@ -64,16 +64,17 @@ public class FPOrWontFixNotificationTest {
       .isNotEqualTo(new FPOrWontFixNotification(new IssuesChangesNotificationBuilder.UserChange(12, user), changedIssues, WONT_FIX))
       .isNotEqualTo(new FPOrWontFixNotification(change, changedIssues, FP));
   }
+
   @Test
   public void hashcode_is_based_on_issues_change_and_resolution() {
-    Rule rule = new Rule(RuleKey.of("repo", "rule_key"), "rule_name");
+    Rule rule = newRandomNotAHotspotRule("rule_name");
     Project project = new Project.Builder("prj_uuid").setKey("prj_key").setProjectName("prj_name").build();
     Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(i -> new ChangedIssue.Builder("key_" + i)
         .setNewStatus("status")
         .setRule(rule)
         .setProject(project)
-      .build())
+        .build())
       .collect(Collectors.toSet());
     AnalysisChange change = new AnalysisChange(12);
     User user = new User("uuid", "login", null);
index c0479b9f3873fdbc7441adfa8733805432659352..38e3ad3f6eefcc018262bf32ac644f077972770d 100644 (file)
@@ -34,6 +34,7 @@ import org.junit.runner.RunWith;
 import org.sonar.api.config.EmailSettings;
 import org.sonar.api.notifications.Notification;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
 import org.sonar.core.i18n.I18n;
 import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix;
 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
@@ -51,8 +52,12 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
 import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP;
 import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newSecurityHotspotRule;
+import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.randomRuleTypeHotspotExcluded;
 
 @RunWith(DataProviderRunner.class)
 public class FpOrWontFixEmailTemplateTest {
@@ -160,7 +165,7 @@ public class FpOrWontFixEmailTemplateTest {
     Project project = newProject("1");
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
-    ChangedIssue changedIssue = newChangedIssue("key", project, ruleName);
+    ChangedIssue changedIssue = newChangedIssue("key", project, ruleName, randomRuleTypeHotspotExcluded());
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
@@ -174,6 +179,26 @@ public class FpOrWontFixEmailTemplateTest {
       .noMoreBlock();
   }
 
+  @Test
+  @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+  public void formats_returns_html_message_for_single_hotspot_on_master(Change change, FpOrWontFix fpOrWontFix) {
+    Project project = newProject("1");
+    String ruleName = randomAlphabetic(8);
+    String host = randomAlphabetic(15);
+    ChangedIssue changedIssue = newChangedIssue("key", project, ruleName, SECURITY_HOTSPOT);
+    when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+    EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
+
+    HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+      .hasParagraph().hasParagraph() // skip header
+      .hasParagraph(project.getProjectName())
+      .hasList("Rule " + ruleName + " - See the single hotspot")
+      .withLink("See the single hotspot", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
+      .hasParagraph().hasParagraph() // skip footer
+      .noMoreBlock();
+  }
+
   @Test
   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
   public void formats_returns_html_message_for_single_issue_on_branch(Change change, FpOrWontFix fpOrWontFix) {
@@ -182,7 +207,7 @@ public class FpOrWontFixEmailTemplateTest {
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
     String key = "key";
-    ChangedIssue changedIssue = newChangedIssue(key, project, ruleName);
+    ChangedIssue changedIssue = newChangedIssue(key, project, ruleName, randomRuleTypeHotspotExcluded());
     when(emailSettings.getServerBaseURL()).thenReturn(host);
 
     EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
@@ -197,13 +222,36 @@ public class FpOrWontFixEmailTemplateTest {
       .noMoreBlock();
   }
 
+  @Test
+  @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+  public void formats_returns_html_message_for_single_hotspot_on_branch(Change change, FpOrWontFix fpOrWontFix) {
+    String branchName = randomAlphabetic(6);
+    Project project = newBranch("1", branchName);
+    String ruleName = randomAlphabetic(8);
+    String host = randomAlphabetic(15);
+    String key = "key";
+    ChangedIssue changedIssue = newChangedIssue(key, project, ruleName, SECURITY_HOTSPOT);
+    when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+    EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix));
+
+    HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+      .hasParagraph().hasParagraph() // skip header
+      .hasParagraph(project.getProjectName() + ", " + branchName)
+      .hasList("Rule " + ruleName + " - See the single hotspot")
+      .withLink("See the single hotspot",
+        host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey())
+      .hasParagraph().hasParagraph() // skip footer
+      .noMoreBlock();
+  }
+
   @Test
   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
   public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master(Change change, FpOrWontFix fpOrWontFix) {
     Project project = newProject("1");
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
-    Rule rule = newRule(ruleName);
+    Rule rule = newRandomNotAHotspotRule(ruleName);
     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
       .collect(toList());
@@ -223,6 +271,32 @@ public class FpOrWontFixEmailTemplateTest {
       .noMoreBlock();
   }
 
+  @Test
+  @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+  public void formats_returns_html_message_for_multiple_hotspots_of_same_rule_on_same_project_on_master(Change change, FpOrWontFix fpOrWontFix) {
+    Project project = newProject("1");
+    String ruleName = randomAlphabetic(8);
+    String host = randomAlphabetic(15);
+    Rule rule = newSecurityHotspotRule(ruleName);
+    List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+      .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
+      .collect(toList());
+    when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+    EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix));
+
+    String expectedHref = host + "/project/issues?id=" + project.getKey()
+      + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
+    String expectedLinkText = "See all " + changedIssues.size() + " hotspots";
+    HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+      .hasParagraph().hasParagraph() // skip header
+      .hasParagraph(project.getProjectName())
+      .hasList("Rule " + ruleName + " - " + expectedLinkText)
+      .withLink(expectedLinkText, expectedHref)
+      .hasParagraph().hasParagraph() // skip footer
+      .noMoreBlock();
+  }
+
   @Test
   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
   public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch(Change change, FpOrWontFix fpOrWontFix) {
@@ -230,7 +304,7 @@ public class FpOrWontFixEmailTemplateTest {
     Project project = newBranch("1", branchName);
     String ruleName = randomAlphabetic(8);
     String host = randomAlphabetic(15);
-    Rule rule = newRule(ruleName);
+    Rule rule = newRandomNotAHotspotRule(ruleName);
     List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
       .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
       .collect(toList());
@@ -250,6 +324,33 @@ public class FpOrWontFixEmailTemplateTest {
       .noMoreBlock();
   }
 
+  @Test
+  @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
+  public void formats_returns_html_message_for_multiple_hotspots_of_same_rule_on_same_project_on_branch(Change change, FpOrWontFix fpOrWontFix) {
+    String branchName = randomAlphabetic(19);
+    Project project = newBranch("1", branchName);
+    String ruleName = randomAlphabetic(8);
+    String host = randomAlphabetic(15);
+    Rule rule = newSecurityHotspotRule(ruleName);
+    List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5))
+      .mapToObj(i -> newChangedIssue("issue_" + i, project, rule))
+      .collect(toList());
+    when(emailSettings.getServerBaseURL()).thenReturn(host);
+
+    EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix));
+
+    String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName
+      + "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C"));
+    String expectedLinkText = "See all " + changedIssues.size() + " hotspots";
+    HtmlFragmentAssert.assertThat(emailMessage.getMessage())
+      .hasParagraph().hasParagraph() // skip header
+      .hasParagraph(project.getProjectName() + ", " + branchName)
+      .hasList("Rule " + ruleName + " - " + expectedLinkText)
+      .withLink(expectedLinkText, expectedHref)
+      .hasParagraph().hasParagraph() // skip footer
+      .noMoreBlock();
+  }
+
   @Test
   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
   public void formats_returns_html_message_with_projects_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) {
@@ -261,7 +362,7 @@ public class FpOrWontFixEmailTemplateTest {
     Project project3 = newProject("C");
     String host = randomAlphabetic(15);
     List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3)
-      .map(project -> newChangedIssue("issue_" + project.getUuid(), project, newRule(randomAlphabetic(2))))
+      .map(project -> newChangedIssue("issue_" + project.getUuid(), project, newRandomNotAHotspotRule(randomAlphabetic(2))))
       .collect(toList());
     Collections.shuffle(changedIssues);
     when(emailSettings.getServerBaseURL()).thenReturn(host);
@@ -290,10 +391,10 @@ public class FpOrWontFixEmailTemplateTest {
   @UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange")
   public void formats_returns_html_message_with_rules_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) {
     Project project = newProject("1");
-    Rule rule1 = newRule("1");
-    Rule rule2 = newRule("a");
-    Rule rule3 = newRule("b");
-    Rule rule4 = newRule("X");
+    Rule rule1 = newRandomNotAHotspotRule("1");
+    Rule rule2 = newRandomNotAHotspotRule("a");
+    Rule rule3 = newRandomNotAHotspotRule("b");
+    Rule rule4 = newRandomNotAHotspotRule("X");
     String host = randomAlphabetic(15);
     List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4)
       .map(rule -> newChangedIssue("issue_" + rule.getName(), project, rule))
@@ -321,8 +422,8 @@ public class FpOrWontFixEmailTemplateTest {
     Project project1 = newProject("1");
     Project project2 = newProject("V");
     Project project2Branch = newBranch("V", "AB");
-    Rule rule1 = newRule("1");
-    Rule rule2 = newRule("a");
+    Rule rule1 = newRandomNotAHotspotRule("1");
+    Rule rule2 = newRandomNotAHotspotRule("a");
     String host = randomAlphabetic(15);
     List<ChangedIssue> changedIssues = Stream.of(
       IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, project1, rule1)),
@@ -393,8 +494,8 @@ public class FpOrWontFixEmailTemplateTest {
     };
   }
 
-  private static ChangedIssue newChangedIssue(String key, Project project, String ruleName) {
-    return newChangedIssue(key, project, newRule(ruleName));
+  private static ChangedIssue newChangedIssue(String key, Project project, String ruleName, RuleType ruleType) {
+    return newChangedIssue(key, project, newRule(ruleName, ruleType));
   }
 
   private static ChangedIssue newChangedIssue(String key, Project project, Rule rule) {
@@ -405,8 +506,8 @@ public class FpOrWontFixEmailTemplateTest {
       .build();
   }
 
-  private static Rule newRule(String ruleName) {
-    return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleName);
+  private static Rule newRule(String ruleName, RuleType ruleType) {
+    return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleType, ruleName);
   }
 
   private static Project newProject(String uuid) {
index ee1727a040759ad412b15dd85408c4a868aadb03..2d2c39e1f06cebacbbf94306347c18e309fcefdc 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.issue.notification;
 
 import java.util.Random;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.BranchDto;
 import org.sonar.db.component.ComponentDto;
@@ -36,15 +37,24 @@ import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.sonar.api.rules.RuleType.BUG;
+import static org.sonar.api.rules.RuleType.CODE_SMELL;
+import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
+import static org.sonar.api.rules.RuleType.VULNERABILITY;
 
 public class IssuesChangesNotificationBuilderTesting {
 
+  private static final RuleType[] RULE_TYPES = {CODE_SMELL, BUG, VULNERABILITY, SECURITY_HOTSPOT};
+
+  private IssuesChangesNotificationBuilderTesting() {
+  }
+
   public static Rule ruleOf(RuleDto rule) {
-    return new Rule(rule.getKey(), rule.getName());
+    return new Rule(rule.getKey(), RuleType.valueOfNullable(rule.getType()), rule.getName());
   }
 
   public static Rule ruleOf(RuleDefinitionDto rule) {
-    return new Rule(rule.getKey(), rule.getName());
+    return new Rule(rule.getKey(), RuleType.valueOfNullable(rule.getType()), rule.getName());
   }
 
   public static User userOf(UserDto changeAuthor) {
@@ -76,8 +86,8 @@ public class IssuesChangesNotificationBuilderTesting {
       .build();
   }
 
-  static ChangedIssue newChangedIssue(String key, String status, Project project, String ruleName) {
-    return newChangedIssue(key, status, project, newRule(ruleName));
+  static ChangedIssue newChangedIssue(String key, String status, Project project, String ruleName, RuleType ruleType) {
+    return newChangedIssue(key, status, project, newRule(ruleName, ruleType));
   }
 
   static ChangedIssue newChangedIssue(String key, String status, Project project, Rule rule) {
@@ -88,8 +98,16 @@ public class IssuesChangesNotificationBuilderTesting {
       .build();
   }
 
-  static Rule newRule(String ruleName) {
-    return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleName);
+  static Rule newRule(String ruleName, RuleType ruleType) {
+    return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleType, ruleName);
+  }
+
+  static Rule newRandomNotAHotspotRule(String ruleName) {
+    return newRule(ruleName, randomRuleTypeHotspotExcluded());
+  }
+
+  static Rule newSecurityHotspotRule(String ruleName) {
+    return newRule(ruleName, SECURITY_HOTSPOT);
   }
 
   static Project newProject(String uuid) {
@@ -107,4 +125,8 @@ public class IssuesChangesNotificationBuilderTesting {
   static AnalysisChange newAnalysisChange() {
     return new AnalysisChange(new Random().nextLong());
   }
+
+  static RuleType randomRuleTypeHotspotExcluded() {
+    return RULE_TYPES[new Random().nextInt(RULE_TYPES.length - 1)];
+  }
 }
index d67f119cbd704322179a8105bdd557d6134b86b7..26ffc7aacb404c4044a14463f41e169b5f702dce 100644 (file)
@@ -306,7 +306,7 @@ public class BulkChangeAction implements IssuesWsAction {
       .setNewStatus(issue.status())
       .setNewResolution(issue.resolution())
       .setAssignee(assignee.map(u -> new User(u.getUuid(), u.getLogin(), u.getName())).orElse(null))
-      .setRule(new IssuesChangesNotificationBuilder.Rule(ruleDefinitionDto.getKey(), ruleDefinitionDto.getName()))
+      .setRule(new IssuesChangesNotificationBuilder.Rule(ruleDefinitionDto.getKey(), RuleType.valueOfNullable(ruleDefinitionDto.getType()), ruleDefinitionDto.getName()))
       .setProject(new Project.Builder(projectDto.uuid())
         .setKey(projectDto.getKey())
         .setProjectName(projectDto.name())
index c393ce34697d9ff1c460248dced472c160b27f0c..0e01c13fa7e066f15f7a7cb563b1afc2fd4542f3 100644 (file)
@@ -24,6 +24,7 @@ import java.util.Optional;
 import javax.annotation.Nullable;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.RuleStatus;
+import org.sonar.api.rules.RuleType;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.IssueChangeContext;
 import org.sonar.core.util.stream.MoreCollectors;
@@ -112,7 +113,7 @@ public class IssueUpdater {
         .setNewResolution(issue.resolution())
         .setNewStatus(issue.status())
         .setAssignee(assignee.map(assigneeDto -> new User(assigneeDto.getUuid(), assigneeDto.getLogin(), assigneeDto.getName())).orElse(null))
-        .setRule(rule.map(r -> new Rule(r.getKey(), r.getName())).get())
+        .setRule(rule.map(r -> new Rule(r.getKey(), RuleType.valueOfNullable(r.getType()), r.getName())).get())
         .setProject(new Project.Builder(project.uuid())
           .setKey(project.getKey())
           .setProjectName(project.name())
index 4d815040df95db04238567f32838d5a1ac444737..34fb7b43a10835943a4f45097691dfe62e6243e4 100644 (file)
@@ -1608,7 +1608,7 @@ email_configuration.test.email_was_sent_to_x=Email was sent to {0}
 #------------------------------------------------------------------------------
 notification.channel.EmailNotificationChannel=Email
 notification.dispatcher.information=A notification is never sent to the author of the event.
-notification.dispatcher.ChangesOnMyIssue=Changes in issues assigned to me
+notification.dispatcher.ChangesOnMyIssue=Changes in issues/hotspots assigned to me
 notification.dispatcher.NewIssues=New issues
 notification.dispatcher.NewAlerts=New quality gate status
 notification.dispatcher.NewFalsePositiveIssue=Issues resolved as false positive or won't fix
index a8012e26cf6d4af89ff2a1c90d852c7d5abd756e..69dca215e83479886d4dbe6af03bed5b37a21bde 100644 (file)
@@ -61,7 +61,7 @@ public enum RuleType {
     }
     throw new IllegalArgumentException(format("Unsupported type value : %d", dbConstant));
   }
-  
+
   @CheckForNull
   public static RuleType valueOfNullable(int dbConstant) {
     // iterating the array is fast-enough as size is small. No need for a map.
@@ -75,5 +75,4 @@ public enum RuleType {
     }
     throw new IllegalArgumentException(format("Unsupported type value : %d", dbConstant));
   }
-
 }