]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20977 Fix notifications sent twice for FP or accepted issues
authorLéo Geoffroy <leo.geoffroy@sonarsource.com>
Wed, 22 Nov 2023 11:18:26 +0000 (12:18 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 23 Nov 2023 20:02:58 +0000 (20:02 +0000)
20 files changed:
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandler.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/test/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandlerTest.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/AddCommentActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/AssignActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/hotspot/ws/ChangeStatusActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/BulkChangeActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/issue/ws/IssueUpdaterIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AddCommentAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/AssignAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/ChangeStatusAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/AssignAction.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/DoTransitionAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java

index da1e5ceb7c88df99a1870fa78f8ea7dd111ade66..c320d38e38d5e45dbc91f024eb4d4db21e183053 100644 (file)
@@ -86,8 +86,8 @@ public class FPOrAcceptedNotificationHandler extends EmailNotificationHandler<Is
       .map(serializer::from)
       // ignore notifications which contain no issue changed to a FP or Accepted status
       .filter(t -> t.getIssues().stream()
-        .filter(issue -> issue.getNewIssueStatus().isPresent())
-        .anyMatch(issue -> FP_OR_ACCEPTED_SIMPLE_STATUSES.contains(issue.getNewIssueStatus().get())))
+        .filter(issue -> issue.getNewIssueStatus().isPresent() && issue.getOldIssueStatus().isPresent())
+        .anyMatch(issue -> !issue.getNewIssueStatus().equals(issue.getOldIssueStatus()) && FP_OR_ACCEPTED_SIMPLE_STATUSES.contains(issue.getNewIssueStatus().get())))
       .map(NotificationWithProjectKeys::new)
       .collect(Collectors.toSet());
     if (changeNotificationsWithFpOrAccepted.isEmpty()) {
@@ -144,14 +144,14 @@ public class FPOrAcceptedNotificationHandler extends EmailNotificationHandler<Is
           .collect(unorderedIndex(t -> t.getNewIssueStatus().get(), issue -> issue));
 
         return Stream.of(
-          of(issuesByNewIssueStatus.get(IssueStatus.FALSE_POSITIVE))
-            .filter(t -> !t.isEmpty())
-            .map(fpIssues -> new FPOrAcceptedNotification(notification.getChange(), fpIssues, FP))
-            .orElse(null),
-          of(issuesByNewIssueStatus.get(IssueStatus.ACCEPTED))
-            .filter(t -> !t.isEmpty())
-            .map(acceptedIssues -> new FPOrAcceptedNotification(notification.getChange(), acceptedIssues, ACCEPTED))
-            .orElse(null))
+            of(issuesByNewIssueStatus.get(IssueStatus.FALSE_POSITIVE))
+              .filter(t -> !t.isEmpty())
+              .map(fpIssues -> new FPOrAcceptedNotification(notification.getChange(), fpIssues, FP))
+              .orElse(null),
+            of(issuesByNewIssueStatus.get(IssueStatus.ACCEPTED))
+              .filter(t -> !t.isEmpty())
+              .map(acceptedIssues -> new FPOrAcceptedNotification(notification.getChange(), acceptedIssues, ACCEPTED))
+              .orElse(null))
           .filter(Objects::nonNull)
           .map(fpOrAcceptedNotification -> new EmailDeliveryRequest(recipient.email(), fpOrAcceptedNotification));
       });
index 7537a83a2edbfa8cec6c990d7b5246595a042e0f..b5a9fdbe48195057cc320fd4c4311c86af3353d5 100644 (file)
@@ -65,11 +65,13 @@ public class IssuesChangesNotificationBuilder {
     private final User assignee;
     private final Rule rule;
     private final Project project;
+    private final IssueStatus oldIssueStatus;
 
     public ChangedIssue(Builder builder) {
       this.key = requireNonNull(builder.key, KEY_CANT_BE_NULL_MESSAGE);
       this.newStatus = requireNonNull(builder.newStatus, "newStatus can't be null");
       this.newIssueStatus = builder.newIssueStatus;
+      this.oldIssueStatus = builder.oldIssueStatus;
       this.assignee = builder.assignee;
       this.rule = requireNonNull(builder.rule, "rule can't be null");
       this.project = requireNonNull(builder.project, "project can't be null");
@@ -87,6 +89,10 @@ public class IssuesChangesNotificationBuilder {
       return ofNullable(newIssueStatus);
     }
 
+    public Optional<IssueStatus> getOldIssueStatus() {
+      return ofNullable(oldIssueStatus);
+    }
+
     public Optional<User> getAssignee() {
       return ofNullable(assignee);
     }
@@ -103,6 +109,8 @@ public class IssuesChangesNotificationBuilder {
       private final String key;
       @CheckForNull
       private IssueStatus newIssueStatus;
+      @CheckForNull
+      private IssueStatus oldIssueStatus;
       private String newStatus;
       @CheckForNull
       private User assignee;
@@ -128,6 +136,11 @@ public class IssuesChangesNotificationBuilder {
         return this;
       }
 
+      public Builder setOldIssueStatus(@Nullable IssueStatus oldIssueStatus) {
+        this.oldIssueStatus = oldIssueStatus;
+        return this;
+      }
+
       public Builder setRule(Rule rule) {
         this.rule = rule;
         return this;
index fad0abbf8fba5f5bd7eae437ace6e46f9786d45d..926c1740bc21503eebe62ffc1eefb144ad2df2f9 100644 (file)
@@ -89,6 +89,7 @@ public class IssuesChangesNotificationSerializer {
       .map(issue -> new ChangedIssue.Builder(issue.key)
         .setNewStatus(issue.newStatus)
         .setNewIssueStatus(issue.newIssueStatus == null ? null : IssueStatus.valueOf(issue.newIssueStatus))
+        .setOldIssueStatus(issue.oldIssueStatus == null ? null : IssueStatus.valueOf(issue.oldIssueStatus))
         .setAssignee(issue.assignee)
         .setRule(rules.get(issue.ruleKey))
         .setProject(projects.get(issue.projectUuid))
@@ -125,6 +126,8 @@ public class IssuesChangesNotificationSerializer {
     notification.setFieldValue(issuePropertyPrefix + ".newStatus", issue.getNewStatus());
     issue.getNewIssueStatus()
       .ifPresent(newIssueStatus -> notification.setFieldValue(issuePropertyPrefix + ".newIssueStatus", newIssueStatus.name()));
+    issue.getOldIssueStatus()
+      .ifPresent(oldIssueStatus -> notification.setFieldValue(issuePropertyPrefix + ".oldIssueStatus", oldIssueStatus.name()));
     notification.setFieldValue(issuePropertyPrefix + ".ruleKey", issue.getRule().getKey().toString());
     notification.setFieldValue(issuePropertyPrefix + ".projectUuid", issue.getProject().getUuid());
   }
@@ -137,6 +140,7 @@ public class IssuesChangesNotificationSerializer {
       .setNewStatus(getIssueFieldValue(notification, issuePropertyPrefix + ".newStatus", index))
       .setNewResolution(notification.getFieldValue(issuePropertyPrefix + ".newResolution"))
       .setNewIssueStatus(notification.getFieldValue(issuePropertyPrefix + ".newIssueStatus"))
+      .setOldIssueStatus(notification.getFieldValue(issuePropertyPrefix + ".oldIssueStatus"))
       .setAssignee(assignee)
       .setRuleKey(getIssueFieldValue(notification, issuePropertyPrefix + ".ruleKey", index))
       .setProjectUuid(getIssueFieldValue(notification, issuePropertyPrefix + ".projectUuid", index))
@@ -254,6 +258,8 @@ public class IssuesChangesNotificationSerializer {
     @CheckForNull
     private final String newIssueStatus;
     @CheckForNull
+    private final String oldIssueStatus;
+    @CheckForNull
     private final User assignee;
     private final RuleKey ruleKey;
     private final String projectUuid;
@@ -266,9 +272,11 @@ public class IssuesChangesNotificationSerializer {
       this.ruleKey = RuleKey.parse(builder.ruleKey);
       this.projectUuid = builder.projectUuid;
       this.newIssueStatus = builder.newIssueStatus;
+      this.oldIssueStatus = builder.oldIssueStatus;
     }
 
     static class Builder {
+      private String oldIssueStatus = null;
       private String key = null;
       private String newStatus = null;
       @CheckForNull
@@ -299,6 +307,11 @@ public class IssuesChangesNotificationSerializer {
         return this;
       }
 
+      public Builder setOldIssueStatus(@Nullable String oldIssueStatus) {
+        this.oldIssueStatus = oldIssueStatus;
+        return this;
+      }
+
       public Builder setAssignee(@Nullable User assignee) {
         this.assignee = assignee;
         return this;
index b53f06b8a3e639293b2c66640f69aee6895e972a..53f94e896c876d55295c589304fcdf4b48757bf7 100644 (file)
@@ -24,7 +24,6 @@ import com.google.common.collect.ListMultimap;
 import com.tngtech.java.junit.dataprovider.DataProvider;
 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
 import com.tngtech.java.junit.dataprovider.UseDataProvider;
-import java.util.Random;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.IntStream;
@@ -62,7 +61,6 @@ import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 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;
@@ -114,7 +112,7 @@ public class FPOrAcceptedNotificationHandlerTest {
   @Test
   public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
     when(emailNotificationChannel.isActivated()).thenReturn(false);
-    Set<IssuesChangesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
+    Set<IssuesChangesNotification> notifications = IntStream.range(0, 5)
       .mapToObj(i -> mock(IssuesChangesNotification.class))
       .collect(toSet());
 
@@ -129,7 +127,7 @@ public class FPOrAcceptedNotificationHandlerTest {
 
   @Test
   public void deliver_parses_every_notification_in_order() {
-    Set<IssuesChangesNotification> notifications = IntStream.range(0, 5 + new Random().nextInt(10))
+    Set<IssuesChangesNotification> notifications = IntStream.range(0, 10)
       .mapToObj(i -> mock(IssuesChangesNotification.class))
       .collect(toSet());
     when(emailNotificationChannel.isActivated()).thenReturn(true);
@@ -143,7 +141,7 @@ public class FPOrAcceptedNotificationHandlerTest {
 
   @Test
   public void deliver_fails_with_IAE_if_serializer_throws_IAE() {
-    Set<IssuesChangesNotification> notifications = IntStream.range(0, 3 + new Random().nextInt(10))
+    Set<IssuesChangesNotification> notifications = IntStream.range(0, 10)
       .mapToObj(i -> mock(IssuesChangesNotification.class))
       .collect(toSet());
     when(emailNotificationChannel.isActivated()).thenReturn(true);
@@ -168,8 +166,31 @@ public class FPOrAcceptedNotificationHandlerTest {
   public void deliver_has_no_effect_if_no_issue_has_FP_or_wontfix_resolution(IssueStatus newIssueStatus) {
     when(emailNotificationChannel.isActivated()).thenReturn(true);
     Change changeMock = mock(Change.class);
-    Set<IssuesChangesNotification> notifications = IntStream.range(0, 2 + new Random().nextInt(5))
-      .mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setNewIssueStatus(newIssueStatus)).collect(toSet()), changeMock))
+    Set<IssuesChangesNotification> notifications = IntStream.range(0, 10)
+      .mapToObj(j -> new IssuesChangesNotificationBuilder(streamOfIssues(t -> t.setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()), changeMock))
+      .map(serializer::serialize)
+      .collect(toSet());
+    reset(serializer);
+
+    int deliver = underTest.deliver(notifications);
+
+    assertThat(deliver).isZero();
+    verify(serializer, times(notifications.size())).from(any(IssuesChangesNotification.class));
+    verifyNoInteractions(changeMock);
+    verifyNoMoreInteractions(serializer);
+    verifyNoInteractions(notificationManager);
+    verify(emailNotificationChannel).isActivated();
+    verifyNoMoreInteractions(emailNotificationChannel);
+  }
+
+  @Test
+  @UseDataProvider("FPorWontFixResolutionWithCorrespondingIssueStatus")
+  public void deliver_shouldNotSendNotification_WhenIssueStatusHasNotChanged(String newResolution,
+    IssueStatus newIssueStatus) {
+    when(emailNotificationChannel.isActivated()).thenReturn(true);
+    Change changeMock = mock(Change.class);
+    Set<IssuesChangesNotification> notifications = IntStream.range(0, 5)
+      .mapToObj(j -> new IssuesChangesNotificationBuilder(streamOfIssues(t -> t.setNewIssueStatus(newIssueStatus).setOldIssueStatus(newIssueStatus)).collect(toSet()), changeMock))
       .map(serializer::serialize)
       .collect(toSet());
     reset(serializer);
@@ -204,21 +225,21 @@ public class FPOrAcceptedNotificationHandlerTest {
     Project projectKey4 = newProject(randomAlphabetic(7));
     Change changeMock = mock(Change.class);
     // some notifications with some issues on project1
-    Stream<IssuesChangesNotificationBuilder> project1Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+    Stream<IssuesChangesNotificationBuilder> project1Notifications = IntStream.range(0, 5)
       .mapToObj(j -> new IssuesChangesNotificationBuilder(
-        randomIssues(t -> t.setProject(projectKey1).setNewIssueStatus(newIssueStatus)).collect(toSet()),
+        streamOfIssues(t -> t.setProject(projectKey1).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()),
         changeMock));
     // some notifications with some issues on project2
-    Stream<IssuesChangesNotificationBuilder> project2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+    Stream<IssuesChangesNotificationBuilder> project2Notifications = IntStream.range(0, 5)
       .mapToObj(j -> new IssuesChangesNotificationBuilder(
-        randomIssues(t -> t.setProject(projectKey2).setNewIssueStatus(newIssueStatus)).collect(toSet()),
+        streamOfIssues(t -> t.setProject(projectKey2).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()),
         changeMock));
     // some notifications with some issues on project3 and project 4
-    Stream<IssuesChangesNotificationBuilder> project3And4Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+    Stream<IssuesChangesNotificationBuilder> project3And4Notifications = IntStream.range(0, 5)
       .mapToObj(j -> new IssuesChangesNotificationBuilder(
         Stream.concat(
-          randomIssues(t -> t.setProject(projectKey3).setNewIssueStatus(newIssueStatus)),
-          randomIssues(t -> t.setProject(projectKey4).setNewIssueStatus(newIssueStatus)))
+            streamOfIssues(t -> t.setProject(projectKey3).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)),
+            streamOfIssues(t -> t.setProject(projectKey4).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)))
           .collect(toSet()),
         changeMock));
     when(emailNotificationChannel.isActivated()).thenReturn(true);
@@ -250,40 +271,40 @@ public class FPOrAcceptedNotificationHandlerTest {
     User otherChangeAuthor = newUser("otherChangeAuthor");
 
     // subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 only
-    Set<IssuesChangesNotificationBuilder> subscriber1Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+    Set<IssuesChangesNotificationBuilder> subscriber1Notifications = IntStream.range(0, 5)
       .mapToObj(j -> new IssuesChangesNotificationBuilder(
-        randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber2)).collect(toSet()),
+        streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)).collect(toSet()),
         newUserChange(subscriber1)))
       .collect(toSet());
     // subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 and subscriber2
-    Set<IssuesChangesNotificationBuilder> subscriber1and2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+    Set<IssuesChangesNotificationBuilder> subscriber1and2Notifications = IntStream.range(0, 5)
       .mapToObj(j -> new IssuesChangesNotificationBuilder(
         Stream.concat(
-          randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber2)),
-          randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber1)))
+            streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)),
+            streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber1)))
           .collect(toSet()),
         newUserChange(subscriber1)))
       .collect(toSet());
     // subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 only
-    Set<IssuesChangesNotificationBuilder> subscriber2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+    Set<IssuesChangesNotificationBuilder> subscriber2Notifications = IntStream.range(0, 5)
       .mapToObj(j -> new IssuesChangesNotificationBuilder(
-        randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber2)).collect(toSet()),
+        streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)).collect(toSet()),
         newUserChange(subscriber2)))
       .collect(toSet());
     // subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 and subscriber 3
-    Set<IssuesChangesNotificationBuilder> subscriber2And3Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
+    Set<IssuesChangesNotificationBuilder> subscriber2And3Notifications = IntStream.range(0, 5)
       .mapToObj(j -> new IssuesChangesNotificationBuilder(
         Stream.concat(
-          randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber2)),
-          randomIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setAssignee(subscriber3)))
+            streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)),
+            streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber3)))
           .collect(toSet()),
         newUserChange(subscriber2)))
       .collect(toSet());
     // subscriber3 is the changeAuthor of no notification
     // otherChangeAuthor has some notifications
-    Set<IssuesChangesNotificationBuilder> otherChangeAuthorNotifications = IntStream.range(0, 1 + new Random().nextInt(2))
-      .mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setProject(project)
-        .setNewIssueStatus(newIssueStatus)).collect(toSet()),
+    Set<IssuesChangesNotificationBuilder> otherChangeAuthorNotifications = IntStream.range(0, 5)
+      .mapToObj(j -> new IssuesChangesNotificationBuilder(streamOfIssues(t -> t.setProject(project)
+        .setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()),
         newUserChange(otherChangeAuthor)))
       .collect(toSet());
     when(emailNotificationChannel.isActivated()).thenReturn(true);
@@ -292,7 +313,7 @@ public class FPOrAcceptedNotificationHandlerTest {
     when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
       .thenReturn(subscriberLogins.stream().map(FPOrAcceptedNotificationHandlerTest::emailRecipientOf).collect(toSet()));
 
-    int deliveredCount = new Random().nextInt(200);
+    int deliveredCount = 200;
     when(emailNotificationChannel.deliverAll(anySet()))
       .thenReturn(deliveredCount)
       .thenThrow(new IllegalStateException("deliver should be called only once"));
@@ -368,12 +389,12 @@ public class FPOrAcceptedNotificationHandlerTest {
     User changeAuthor = newUser("changeAuthor");
 
     Set<ChangedIssue> fpIssues = projects.stream()
-      .flatMap(project -> randomIssues(t -> t.setProject(project)
-        .setNewIssueStatus(IssueStatus.FALSE_POSITIVE).setAssignee(subscriber1)))
+      .flatMap(project -> streamOfIssues(t -> t.setProject(project)
+        .setNewIssueStatus(IssueStatus.FALSE_POSITIVE).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber1)))
       .collect(toSet());
     Set<ChangedIssue> wontFixIssues = projects.stream()
-      .flatMap(project -> randomIssues(t -> t.setProject(project)
-        .setNewIssueStatus(IssueStatus.ACCEPTED).setAssignee(subscriber1)))
+      .flatMap(project -> streamOfIssues(t -> t.setProject(project)
+        .setNewIssueStatus(IssueStatus.ACCEPTED).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber1)))
       .collect(toSet());
     UserChange userChange = newUserChange(changeAuthor);
     IssuesChangesNotificationBuilder fpAndWontFixNotifications = new IssuesChangesNotificationBuilder(
@@ -383,7 +404,7 @@ public class FPOrAcceptedNotificationHandlerTest {
     projects.forEach(project -> when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
       .thenReturn(singleton(emailRecipientOf(subscriber1.getLogin()))));
 
-    int deliveredCount = new Random().nextInt(200);
+    int deliveredCount = 200;
     when(emailNotificationChannel.deliverAll(anySet()))
       .thenReturn(deliveredCount)
       .thenThrow(new IllegalStateException("deliver should be called only once"));
@@ -414,7 +435,7 @@ public class FPOrAcceptedNotificationHandlerTest {
   public static Object[][] oneOrMoreProjectCounts() {
     return new Object[][] {
       {1},
-      {2 + new Random().nextInt(3)},
+      {5},
     };
   }
 
@@ -452,8 +473,8 @@ public class FPOrAcceptedNotificationHandlerTest {
     };
   }
 
-  private static Stream<ChangedIssue> randomIssues(Consumer<ChangedIssue.Builder> consumer) {
-    return IntStream.range(0, 1 + new Random().nextInt(5))
+  private static Stream<ChangedIssue> streamOfIssues(Consumer<ChangedIssue.Builder> consumer) {
+    return IntStream.range(0, 5)
       .mapToObj(i -> {
         ChangedIssue.Builder builder = new ChangedIssue.Builder("key_" + i)
           .setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5)))
index b28fc33bb0dc36712dfc5b17e5d8f6673564b20a..220033eff43a09e767bd862d7fd7e1c6aaec6e93 100644 (file)
@@ -242,6 +242,7 @@ public class AddCommentActionIT {
     verify(issueFieldsSetter).addComment(defaultIssueCaptor.capture(), eq(comment), eq(issueChangeContext));
     verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
       any(DbSession.class),
+      any(IssueDto.class),
       defaultIssueCaptor.capture(),
       eq(issueChangeContext));
 
index 7144defe8da7eb2ceef335178513cf59e2a620d5..886e49660c8436943874ad237b3098ace28fc144 100644 (file)
@@ -566,6 +566,7 @@ public class AssignActionIT {
     verify(issueFieldsSetter).assign(defaultIssueCaptor.capture(), userMatcher(assignee), any(IssueChangeContext.class));
     verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
       any(DbSession.class),
+      any(IssueDto.class),
       defaultIssueCaptor.capture(),
       any(IssueChangeContext.class));
 
index f45894b058e9c0705c54bc1efbdc683df236c674..1a7e2f9aa4b94d35082f59358e1bdedfb7933124 100644 (file)
@@ -441,6 +441,7 @@ public class ChangeStatusActionIT {
     if (transitionDone) {
       verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
         any(DbSession.class),
+        any(IssueDto.class),
         defaultIssueCaptor.capture(),
         eq(issueChangeContext));
 
@@ -546,6 +547,7 @@ public class ChangeStatusActionIT {
     if (transitionDone) {
       verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
         any(DbSession.class),
+        any(IssueDto.class),
         defaultIssueCaptor.capture(),
         eq(issueChangeContext));
 
@@ -595,6 +597,7 @@ public class ChangeStatusActionIT {
       verify(issueFieldsSetter).addComment(defaultIssueCaptor.capture(), eq(comment), eq(issueChangeContext));
       verify(issueUpdater).saveIssueAndPreloadSearchResponseData(
         any(DbSession.class),
+        any(IssueDto.class),
         defaultIssueCaptor.capture(),
         eq(issueChangeContext));
 
index 8a0953c6de7a316b930b7e098ff77f8d30d5b83a..9249c8a56d5a6e90a978dfdb6f3b1fbfa9e4378b 100644 (file)
@@ -32,6 +32,7 @@ import org.sonar.api.impl.utils.TestSystem2;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.System2;
+import org.sonar.core.issue.status.IssueStatus;
 import org.sonar.core.util.SequenceUuidFactory;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbTester;
@@ -418,6 +419,8 @@ public class BulkChangeActionIT {
     ChangedIssue changedIssue = builder.getIssues().iterator().next();
     assertThat(changedIssue.getKey()).isEqualTo(issue.getKey());
     assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_CONFIRMED);
+    assertThat(changedIssue.getNewIssueStatus()).contains(IssueStatus.CONFIRMED);
+    assertThat(changedIssue.getOldIssueStatus()).contains(IssueStatus.OPEN);
     assertThat(changedIssue.getAssignee()).isEmpty();
     assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule));
     assertThat(changedIssue.getProject()).isEqualTo(projectBranchOf(db, branch));
@@ -484,6 +487,8 @@ public class BulkChangeActionIT {
     ChangedIssue changedIssue = builder.getIssues().iterator().next();
     assertThat(changedIssue.getKey()).isEqualTo(issue3.getKey());
     assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_OPEN);
+    assertThat(changedIssue.getNewIssueStatus()).contains(IssueStatus.OPEN);
+    assertThat(changedIssue.getOldIssueStatus()).contains(IssueStatus.OPEN);
     assertThat(changedIssue.getAssignee()).isEmpty();
     assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule));
     assertThat(changedIssue.getProject()).isEqualTo(projectOf(project));
index 5ede61e285816081235f14a4c88f81debf25bcd8..50bb877962adddce8d67fd5d9eadc4629161e1bf 100644 (file)
@@ -55,6 +55,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
+import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
 import static org.sonar.api.rule.Severity.BLOCKER;
 import static org.sonar.api.rule.Severity.MAJOR;
 import static org.sonar.core.issue.IssueChangeContext.issueChangeContextByUserBuilder;
@@ -90,12 +91,13 @@ public class IssueUpdaterIT {
 
   @Test
   public void update_issue() {
-    DefaultIssue issue = db.issues().insertIssue(i -> i.setSeverity(MAJOR)).toDefaultIssue();
+    IssueDto originalIssueDto = db.issues().insertIssue(i -> i.setSeverity(MAJOR));
+    DefaultIssue issue = originalIssueDto.toDefaultIssue();
     UserDto user = db.users().insertUser();
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), user.getUuid()).build();
     issueFieldsSetter.setSeverity(issue, BLOCKER, context);
 
-    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
+    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);
 
     IssueDto issueReloaded = dbClient.issueDao().selectByKey(db.getSession(), issue.key()).get();
     assertThat(issueReloaded.getSeverity()).isEqualTo(BLOCKER);
@@ -107,14 +109,15 @@ public class IssueUpdaterIT {
     RuleDto rule = db.rules().insertIssueRule();
     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
-    DefaultIssue issue = db.issues().insertIssue(rule, project, file,
-      t -> t.setSeverity(MAJOR).setAssigneeUuid(assignee.getUuid()))
+    IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file,
+      t -> t.setSeverity(MAJOR).setAssigneeUuid(assignee.getUuid()));
+    DefaultIssue issue = originalIssueDto
       .toDefaultIssue();
     UserDto changeAuthor = db.users().insertUser();
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).build();
     issueFieldsSetter.setSeverity(issue, BLOCKER, context);
 
-    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
+    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);
 
     verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
     IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue();
@@ -135,14 +138,16 @@ public class IssueUpdaterIT {
     RuleDto rule = db.rules().insertIssueRule();
     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
-    DefaultIssue issue = db.issues().insertIssue(rule, project, file,
-      t -> t.setSeverity(MAJOR).setAssigneeUuid(assignee.getUuid()))
+    IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file,
+      t -> t.setSeverity(MAJOR).setAssigneeUuid(assignee.getUuid()));
+    DefaultIssue issue = originalIssueDto
       .toDefaultIssue();
     UserDto changeAuthor = db.users().insertUser();
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).build();
     issueFieldsSetter.setResolution(issue, RESOLUTION_FIXED, context);
+    issueFieldsSetter.setStatus(issue, STATUS_RESOLVED, context);
 
-    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
+    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);
 
     verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
     IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue();
@@ -151,6 +156,8 @@ public class IssueUpdaterIT {
     ChangedIssue changedIssue = builder.getIssues().iterator().next();
     assertThat(changedIssue.getKey()).isEqualTo(issue.key());
     assertThat(changedIssue.getNewStatus()).isEqualTo(issue.status());
+    assertThat(changedIssue.getOldIssueStatus()).contains(originalIssueDto.getIssueStatus());
+    assertThat(changedIssue.getNewIssueStatus()).contains(issue.getIssueStatus());
     assertThat(changedIssue.getAssignee()).contains(userOf(assignee));
     assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule));
     assertThat(changedIssue.getProject()).isEqualTo(projectOf(project));
@@ -163,13 +170,14 @@ public class IssueUpdaterIT {
     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
     ComponentDto branch = db.components().insertProjectBranch(project, t -> t.setBranchType(BRANCH));
     ComponentDto file = db.components().insertComponent(newFileDto(branch, project.uuid()));
-    DefaultIssue issue = db.issues().insertIssue(rule, branch, file,
-      t -> t.setSeverity(MAJOR)).toDefaultIssue();
+    IssueDto originalIssueDto = db.issues().insertIssue(rule, branch, file,
+      t -> t.setSeverity(MAJOR));
+    DefaultIssue issue = originalIssueDto.toDefaultIssue();
     UserDto changeAuthor = db.users().insertUser();
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).build();
     issueFieldsSetter.setSeverity(issue, BLOCKER, context);
 
-    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
+    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);
 
     verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
     IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue();
@@ -190,11 +198,12 @@ public class IssueUpdaterIT {
     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
     ComponentDto branch = db.components().insertProjectBranch(project, t -> t.setBranchType(BranchType.PULL_REQUEST));
     ComponentDto file = db.components().insertComponent(newFileDto(branch, project.uuid()));
-    DefaultIssue issue = db.issues().insertIssue(rule, branch, file, t -> t.setSeverity(MAJOR)).toDefaultIssue();
+    IssueDto originalIssueDto = db.issues().insertIssue(rule, branch, file, t -> t.setSeverity(MAJOR));
+    DefaultIssue issue = originalIssueDto.toDefaultIssue();
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), "user_uuid").build();
     issueFieldsSetter.setSeverity(issue, BLOCKER, context);
 
-    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
+    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);
 
     verifyNoInteractions(notificationManager);
   }
@@ -204,11 +213,12 @@ public class IssueUpdaterIT {
     RuleDto rule = db.rules().insertIssueRule(r -> r.setStatus(RuleStatus.REMOVED));
     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
-    DefaultIssue issue = db.issues().insertIssue(rule, project, file, t -> t.setSeverity(MAJOR)).toDefaultIssue();
+    IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file, t -> t.setSeverity(MAJOR));
+    DefaultIssue issue = originalIssueDto.toDefaultIssue();
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), "user_uuid").build();
     issueFieldsSetter.setSeverity(issue, BLOCKER, context);
 
-    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
+    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);
 
     verifyNoInteractions(notificationManager);
   }
@@ -219,14 +229,15 @@ public class IssueUpdaterIT {
     RuleDto rule = db.rules().insertIssueRule();
     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
-    DefaultIssue issue = db.issues().insertIssue(rule, project, file, t -> t.setAssigneeUuid(oldAssignee.getUuid()))
+    IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file, t -> t.setAssigneeUuid(oldAssignee.getUuid()));
+    DefaultIssue issue = originalIssueDto
       .toDefaultIssue();
     UserDto changeAuthor = db.users().insertUser();
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).build();
     UserDto newAssignee = db.users().insertUser();
     issueFieldsSetter.assign(issue, newAssignee, context);
 
-    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
+    underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);
 
     verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture());
     IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue();
@@ -246,18 +257,18 @@ public class IssueUpdaterIT {
     RuleDto rule = db.rules().insertIssueRule();
     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
-    IssueDto issueDto = db.issues().insertIssue(rule, project, file);
-    DefaultIssue issue = issueDto.setSeverity(MAJOR).toDefaultIssue();
+    IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file);
+    DefaultIssue issue = originalIssueDto.setSeverity(MAJOR).toDefaultIssue();
     UserDto changeAuthor = db.users().insertUser();
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), changeAuthor.getUuid()).withRefreshMeasures().build();
     issueFieldsSetter.setSeverity(issue, BLOCKER, context);
 
-    SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
+    SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);
 
     assertThat(preloadedSearchResponseData.getIssues())
       .hasSize(1);
     assertThat(preloadedSearchResponseData.getIssues().iterator().next())
-      .isNotSameAs(issueDto);
+      .isNotSameAs(originalIssueDto);
     assertThat(preloadedSearchResponseData.getRules())
       .extracting(RuleDto::getKey)
       .containsOnly(rule.getKey());
@@ -272,17 +283,17 @@ public class IssueUpdaterIT {
     RuleDto rule = db.rules().insertIssueRule(r -> r.setStatus(RuleStatus.REMOVED));
     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
-    IssueDto issueDto = db.issues().insertIssue(rule, project, file);
-    DefaultIssue issue = issueDto.setSeverity(MAJOR).toDefaultIssue();
+    IssueDto originalIssueDto = db.issues().insertIssue(rule, project, file);
+    DefaultIssue issue = originalIssueDto.setSeverity(MAJOR).toDefaultIssue();
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), "user_uuid").build();
     issueFieldsSetter.setSeverity(issue, BLOCKER, context);
 
-    SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context);
+    SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), originalIssueDto, issue, context);
 
     assertThat(preloadedSearchResponseData.getIssues())
       .hasSize(1);
     assertThat(preloadedSearchResponseData.getIssues().iterator().next())
-      .isNotSameAs(issueDto);
+      .isNotSameAs(originalIssueDto);
     assertThat(preloadedSearchResponseData.getRules()).isNullOrEmpty();
     assertThat(preloadedSearchResponseData.getComponents())
       .extracting(ComponentDto::uuid)
index 1db5aba3ef1780d0fe6d4e0eb69a6559489c9b73..0307a96f27b30dde66e800b93e9756e155bade54 100644 (file)
@@ -85,7 +85,7 @@ public class AddCommentAction implements HotspotsWsAction {
       DefaultIssue defaultIssue = hotspot.toDefaultIssue();
       IssueChangeContext context = hotspotWsSupport.newIssueChangeContextWithoutMeasureRefresh();
       issueFieldsSetter.addComment(defaultIssue, comment, context);
-      issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context);
+      issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, hotspot, defaultIssue, context);
       response.noContent();
     }
   }
index b7547af68ae3d877525290d4f768c1b66e30b447..cd6f52ba54622df9a36a361db0ae120cf0fdcf82 100644 (file)
@@ -126,7 +126,7 @@ public class AssignAction implements HotspotsWsAction {
       }
 
       if (issueFieldsSetter.assign(defaultIssue, assignee, context)) {
-        issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context);
+        issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, hotspotDto, defaultIssue, context);
 
         BranchDto branch = issueUpdater.getBranch(dbSession, defaultIssue);
         if (BRANCH.equals(branch.getBranchType())) {
index 7111c7ab1cbe9d56eb28e6d5330108ebda083a54..fc399baf53eac61bf03b8217275782671b51295b 100644 (file)
@@ -162,7 +162,7 @@ public class ChangeStatusAction implements HotspotsWsAction {
         issueFieldsSetter.addComment(defaultIssue, comment, context);
       }
 
-      issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context);
+      issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, defaultIssue, context);
 
       BranchDto branch = issueUpdater.getBranch(session, defaultIssue);
       if (BRANCH.equals(branch.getBranchType())) {
index 0162e74f9a677f0f881a954cb24b7080517cf34d..8672c24bb502cc9421e6f3438be70548d9a3f597 100644 (file)
@@ -103,7 +103,7 @@ public class AddCommentAction implements IssuesWsAction {
       IssueChangeContext context = issueChangeContextByUserBuilder(new Date(system2.now()), userSession.getUuid()).build();
       DefaultIssue defaultIssue = issueDto.toDefaultIssue();
       issueFieldsSetter.addComment(defaultIssue, wsRequest.getText(), context);
-      SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context);
+      SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issueDto, defaultIssue, context);
       responseWriter.write(defaultIssue.key(), preloadedSearchResponseData, request, response);
     }
   }
index 6a8068690e34378d93d4da0a66f5d6b89d8c7ceb..38f135887b5339fc6c9fea9e421d250756c14ae7 100644 (file)
@@ -111,7 +111,7 @@ public class AssignAction implements IssuesWsAction {
       UserDto user = getUser(dbSession, login);
       IssueChangeContext context = issueChangeContextByUserBuilder(new Date(system2.now()), userSession.getUuid()).build();
       if (issueFieldsSetter.assign(issue, user, context)) {
-        return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issue, context);
+        return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issueDto, issue, context);
       }
       return new SearchResponseData(issueDto);
     }
index 70f0939263b1e04e6a6ac5644cee79baca9bb5bc..c348674706bcce47b1e2704fa2e98f9bfcc716a5 100644 (file)
@@ -47,7 +47,6 @@ import org.sonar.api.utils.System2;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.IssueChangeContext;
-import org.sonar.core.issue.status.IssueStatus;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.BranchDto;
@@ -152,7 +151,7 @@ public class BulkChangeAction implements IssuesWsAction {
   public void define(WebService.NewController context) {
     WebService.NewAction action = context.createAction(ACTION_BULK_CHANGE)
       .setDescription("Bulk change on issues. Up to 500 issues can be updated. <br/>" +
-        "Requires authentication.")
+                      "Requires authentication.")
       .setSince("3.7")
       .setChangelog(
         new Change("10.4", ("Transitions '%s' and '%s' are now deprecated. Use transition '%s' instead. " +
@@ -196,7 +195,7 @@ public class BulkChangeAction implements IssuesWsAction {
       .setExampleValue("security,java8");
     action.createParam(PARAM_COMMENT)
       .setDescription("Add a comment. "
-        + "The comment will only be added to issues that are affected either by a change of type or a change of severity as a result of the same WS call.")
+                      + "The comment will only be added to issues that are affected either by a change of type or a change of severity as a result of the same WS call.")
       .setExampleValue("Here is my comment");
     action.createParam(PARAM_SEND_NOTIFICATIONS)
       .setSince("4.0")
@@ -334,11 +333,13 @@ public class BulkChangeAction implements IssuesWsAction {
     if (ruleDefinitionDto == null || projectDto == null) {
       return null;
     }
+    IssueDto oldIssueDto = bulkChangeData.originalIssueByKey.get(issue.key());
 
     Optional<UserDto> assignee = Optional.ofNullable(issue.assignee()).map(userDtoByUuid::get);
     return new ChangedIssue.Builder(issue.key())
       .setNewStatus(issue.status())
-      .setNewIssueStatus(IssueStatus.of(issue.status(), issue.resolution()))
+      .setNewIssueStatus(issue.getIssueStatus())
+      .setOldIssueStatus(oldIssueDto.getIssueStatus())
       .setAssignee(assignee.map(u -> new User(u.getUuid(), u.getLogin(), u.getName())).orElse(null))
       .setRule(new IssuesChangesNotificationBuilder.Rule(ruleDefinitionDto.getKey(), RuleType.valueOfNullable(ruleDefinitionDto.getType()), ruleDefinitionDto.getName()))
       .setProject(new Project.Builder(projectDto.uuid())
@@ -382,6 +383,7 @@ public class BulkChangeAction implements IssuesWsAction {
     private final Map<String, ComponentDto> componentsByUuid;
     private final Map<RuleKey, RuleDto> rulesByKey;
     private final List<Action> availableActions;
+    private final Map<String, IssueDto> originalIssueByKey;
 
     BulkChangeData(DbSession dbSession, Request request) {
       this.sendNotification = request.mandatoryParamAsBoolean(PARAM_SEND_NOTIFICATIONS);
@@ -398,12 +400,14 @@ public class BulkChangeAction implements IssuesWsAction {
       this.branchComponentByUuid = getAuthorizedComponents(allBranches).stream().collect(toMap(ComponentDto::uuid, identity()));
       this.branchesByProjectUuid = dbClient.branchDao().selectByUuids(dbSession, branchComponentByUuid.keySet()).stream()
         .collect(toMap(BranchDto::getUuid, identity()));
-      this.issues = getAuthorizedIssues(allIssues);
+      List<IssueDto> authorizedIssues = getAuthorizedIssues(allIssues);
+      this.originalIssueByKey = authorizedIssues.stream().collect(toMap(IssueDto::getKee, identity()));
+      this.issues = toDefaultIssues(authorizedIssues);
       this.componentsByUuid = getComponents(dbSession,
         issues.stream().map(DefaultIssue::componentUuid).collect(Collectors.toSet())).stream()
-          .collect(toMap(ComponentDto::uuid, identity()));
+        .collect(toMap(ComponentDto::uuid, identity()));
       this.rulesByKey = dbClient.ruleDao().selectByKeys(dbSession,
-        issues.stream().map(DefaultIssue::ruleKey).collect(Collectors.toSet())).stream()
+          issues.stream().map(DefaultIssue::ruleKey).collect(Collectors.toSet())).stream()
         .collect(toMap(RuleDto::getKey, identity()));
 
       this.availableActions = actions.stream()
@@ -420,10 +424,15 @@ public class BulkChangeAction implements IssuesWsAction {
       return userSession.keepAuthorizedComponents(UserRole.USER, projectDtos);
     }
 
-    private List<DefaultIssue> getAuthorizedIssues(List<IssueDto> allIssues) {
+    private List<IssueDto> getAuthorizedIssues(List<IssueDto> allIssues) {
       Set<String> branchUuids = branchComponentByUuid.values().stream().map(ComponentDto::uuid).collect(Collectors.toSet());
       return allIssues.stream()
         .filter(issue -> branchUuids.contains(issue.getProjectUuid()))
+        .toList();
+    }
+
+    private List<DefaultIssue> toDefaultIssues(List<IssueDto> allIssues) {
+      return allIssues.stream()
         .map(IssueDto::toDefaultIssue)
         .toList();
     }
index 06359bf69ee8c699afbcd677015371b50f7f68b3..56ffcf3aee8cf6f305b246eedd89218bdadabda1 100644 (file)
@@ -128,7 +128,7 @@ public class DoTransitionAction implements IssuesWsAction {
     transitionService.checkTransitionPermission(transitionKey, defaultIssue);
     if (transitionService.doTransition(defaultIssue, context, transitionKey)) {
       BranchDto branch = issueUpdater.getBranch(session, defaultIssue);
-      SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, branch);
+      SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, defaultIssue, context, branch);
 
       if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(defaultIssue.projectUuid()) != null) {
         issueChangeEventService.distributeIssueChangeEvent(defaultIssue, null, null, transitionKey, branch,
index b07bcb0e61b70f4421ca0979ca50ad690af097ab..a741dca3474fe47f3352f72c8bb5d00c245d8a44 100644 (file)
@@ -71,16 +71,17 @@ public class IssueUpdater {
     this.notificationSerializer = notificationSerializer;
   }
 
-  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context) {
-    BranchDto branch = getBranch(dbSession, issue);
-    return saveIssueAndPreloadSearchResponseData(dbSession, issue, context, branch);
+  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, IssueDto originalIssue, DefaultIssue newIssue, IssueChangeContext context) {
+    BranchDto branch = getBranch(dbSession, newIssue);
+    return saveIssueAndPreloadSearchResponseData(dbSession, originalIssue, newIssue, context, branch);
   }
 
-  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context, BranchDto branch) {
-    Optional<RuleDto> rule = getRuleByKey(dbSession, issue.getRuleKey());
-    ComponentDto branchComponent = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid());
-    ComponentDto component = getComponent(dbSession, issue, issue.componentUuid());
-    IssueDto issueDto = doSaveIssue(dbSession, issue, context, rule.orElse(null), branchComponent, branch);
+  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, IssueDto originalIssue,
+    DefaultIssue newIssue, IssueChangeContext context, BranchDto branch) {
+    Optional<RuleDto> rule = getRuleByKey(dbSession, newIssue.getRuleKey());
+    ComponentDto branchComponent = dbClient.componentDao().selectOrFailByUuid(dbSession, newIssue.projectUuid());
+    ComponentDto component = getComponent(dbSession, newIssue, newIssue.componentUuid());
+    IssueDto issueDto = doSaveIssue(dbSession, originalIssue, newIssue, context, rule.orElse(null), branchComponent, branch);
 
     SearchResponseData result = new SearchResponseData(issueDto);
     rule.ifPresent(r -> result.addRules(singletonList(r)));
@@ -107,18 +108,18 @@ public class IssueUpdater {
     return branchDto;
   }
 
-  private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context,
+  private IssueDto doSaveIssue(DbSession session, IssueDto originalIssue, DefaultIssue issue, IssueChangeContext context,
     @Nullable RuleDto ruleDto, ComponentDto project, BranchDto branchDto) {
     IssueDto issueDto = issueStorage.save(session, singletonList(issue)).iterator().next();
     Date updateDate = issue.updateDate();
     if (
       // since this method is called after an update of the issue, date should never be null
       updateDate == null
-        // name of rule is displayed in notification, rule must therefor be present
-        || ruleDto == null
-        // notification are not supported on PRs
-        || !hasNotificationSupport(branchDto)
-        || context.getWebhookSource() != null) {
+      // name of rule is displayed in notification, rule must therefor be present
+      || ruleDto == null
+      // notification are not supported on PRs
+      || !hasNotificationSupport(branchDto)
+      || context.getWebhookSource() != null) {
       return issueDto;
     }
 
@@ -131,6 +132,7 @@ public class IssueUpdater {
       new ChangedIssue.Builder(issue.key())
         .setNewStatus(issue.status())
         .setNewIssueStatus(IssueStatus.of(issue.status(), issue.resolution()))
+        .setOldIssueStatus(originalIssue.getIssueStatus())
         .setAssignee(assignee.map(assigneeDto -> new User(assigneeDto.getUuid(), assigneeDto.getLogin(), assigneeDto.getName())).orElse(null))
         .setRule(new Rule(ruleDto.getKey(), RuleType.valueOfNullable(ruleDto.getType()), ruleDto.getName()))
         .setProject(new Project.Builder(project.uuid())
index 9e6ed717050d04741165458ffaeff328601ef591..57dd3200daeb5fc4a5dfff55abfe0b6bbf9e1327 100644 (file)
@@ -71,12 +71,12 @@ public class SetSeverityAction implements IssuesWsAction {
   public void define(WebService.NewController controller) {
     WebService.NewAction action = controller.createAction(ACTION_SET_SEVERITY)
       .setDescription("Change severity.<br/>" +
-        "Requires the following permissions:" +
-        "<ul>" +
-        "  <li>'Authentication'</li>" +
-        "  <li>'Browse' rights on project of the specified issue</li>" +
-        "  <li>'Administer Issues' rights on project of the specified issue</li>" +
-        "</ul>")
+                      "Requires the following permissions:" +
+                      "<ul>" +
+                      "  <li>'Authentication'</li>" +
+                      "  <li>'Browse' rights on project of the specified issue</li>" +
+                      "  <li>'Administer Issues' rights on project of the specified issue</li>" +
+                      "</ul>")
       .setSince("3.6")
       .setChangelog(
         new Change("10.4", "The response fields 'status' and 'resolution' are deprecated. Please use 'issueStatus' instead."),
@@ -121,7 +121,7 @@ public class SetSeverityAction implements IssuesWsAction {
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), userSession.getUuid()).withRefreshMeasures().build();
     if (issueFieldsSetter.setManualSeverity(issue, severity, context)) {
       BranchDto branch = issueUpdater.getBranch(session, issue);
-      SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, branch);
+      SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, issue, context, branch);
 
       if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) {
         issueChangeEventService.distributeIssueChangeEvent(issue, severity, null, null,
index 5933d22ed59355ac5be61f7ca07a4c965b773d25..ab30dbe16f74a7e995b7396fc783cd37501d3822 100644 (file)
@@ -109,7 +109,7 @@ public class SetTagsAction implements IssuesWsAction {
       DefaultIssue issue = issueDto.toDefaultIssue();
       IssueChangeContext context = issueChangeContextByUserBuilder(new Date(), userSession.getUuid()).build();
       if (issueFieldsSetter.setTags(issue, tags, context)) {
-        return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context);
+        return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, issue, context);
       }
       return new SearchResponseData(issueDto);
     }
index 139f274bcfcfec0aed61425d97559aeced1f4efc..61be4928b125cfd1ba67acd2e71065ba0c21a0d6 100644 (file)
@@ -126,7 +126,7 @@ public class SetTypeAction implements IssuesWsAction {
     IssueChangeContext context = issueChangeContextByUserBuilder(new Date(system2.now()), userSession.getUuid()).withRefreshMeasures().build();
     if (issueFieldsSetter.setType(issue, ruleType, context)) {
       BranchDto branch = issueUpdater.getBranch(session, issue);
-      SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, branch);
+      SearchResponseData response = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issueDto, issue, context, branch);
       if (branch.getBranchType().equals(BRANCH) && response.getComponentByUuid(issue.projectUuid()) != null) {
         issueChangeEventService.distributeIssueChangeEvent(issue, null, ruleType.name(), null, branch,
           response.getComponentByUuid(issue.projectUuid()).getKey());