diff options
author | Léo Geoffroy <leo.geoffroy@sonarsource.com> | 2023-11-22 12:18:26 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-11-23 20:02:58 +0000 |
commit | d3c85d42409248e8e0fed7c103358c522d2de193 (patch) | |
tree | 68ebddf5824231ce2a00e3d88885a0572eda8f83 /server/sonar-server-common | |
parent | 94a8b446ce8a36f3410a4c6c92da737f93bce5f8 (diff) | |
download | sonarqube-d3c85d42409248e8e0fed7c103358c522d2de193.tar.gz sonarqube-d3c85d42409248e8e0fed7c103358c522d2de193.zip |
SONAR-20977 Fix notifications sent twice for FP or accepted issues
Diffstat (limited to 'server/sonar-server-common')
4 files changed, 93 insertions, 46 deletions
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandler.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandler.java index da1e5ceb7c8..c320d38e38d 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandler.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandler.java @@ -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)); }); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java index 7537a83a2ed..b5a9fdbe481 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationBuilder.java @@ -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; diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java index fad0abbf8fb..926c1740bc2 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/IssuesChangesNotificationSerializer.java @@ -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; diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandlerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandlerTest.java index b53f06b8a3e..53f94e896c8 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandlerTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/FPOrAcceptedNotificationHandlerTest.java @@ -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))) |