]> source.dussan.org Git - sonarqube.git/blob
fe25236bc074a4a8db83cdd1242a8ebe4899d221
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2024 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 3 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 package org.sonar.server.issue.notification;
21
22 import com.google.common.collect.ImmutableSet;
23 import com.google.common.collect.ListMultimap;
24 import com.tngtech.java.junit.dataprovider.DataProvider;
25 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
26 import com.tngtech.java.junit.dataprovider.UseDataProvider;
27 import java.util.Set;
28 import java.util.function.Consumer;
29 import java.util.stream.IntStream;
30 import java.util.stream.Stream;
31 import org.junit.Test;
32 import org.junit.runner.RunWith;
33 import org.mockito.ArgumentCaptor;
34 import org.mockito.Mockito;
35 import org.sonar.api.issue.Issue;
36 import org.sonar.api.issue.IssueStatus;
37 import org.sonar.server.issue.notification.FPOrAcceptedNotification.FpPrAccepted;
38 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
39 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
40 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
41 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
42 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
43 import org.sonar.server.notification.NotificationDispatcherMetadata;
44 import org.sonar.server.notification.NotificationManager;
45 import org.sonar.server.notification.email.EmailNotificationChannel;
46 import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
47
48 import static java.util.Collections.singleton;
49 import static java.util.stream.Collectors.toSet;
50 import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
51 import static org.assertj.core.api.Assertions.assertThat;
52 import static org.junit.Assert.fail;
53 import static org.mockito.ArgumentMatchers.any;
54 import static org.mockito.ArgumentMatchers.anySet;
55 import static org.mockito.Mockito.mock;
56 import static org.mockito.Mockito.reset;
57 import static org.mockito.Mockito.spy;
58 import static org.mockito.Mockito.times;
59 import static org.mockito.Mockito.verify;
60 import static org.mockito.Mockito.verifyNoInteractions;
61 import static org.mockito.Mockito.verifyNoMoreInteractions;
62 import static org.mockito.Mockito.when;
63 import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
64 import static org.sonar.core.util.stream.MoreCollectors.index;
65 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
66 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
67 import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
68 import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
69 import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
70
71 @RunWith(DataProviderRunner.class)
72 public class FPOrAcceptedNotificationHandlerTest {
73   private static final String DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY = "NewFalsePositiveIssue";
74   private NotificationManager notificationManager = mock(NotificationManager.class);
75   private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
76   private IssuesChangesNotificationSerializer serializerMock = mock(IssuesChangesNotificationSerializer.class);
77   private IssuesChangesNotificationSerializer serializer = spy(new IssuesChangesNotificationSerializer());
78   private Class<Set<EmailDeliveryRequest>> requestSetType = (Class<Set<EmailDeliveryRequest>>) (Class<?>) Set.class;
79   private FPOrAcceptedNotificationHandler underTest = new FPOrAcceptedNotificationHandler(notificationManager, emailNotificationChannel, serializer);
80
81   @Test
82   public void getMetadata_returns_same_instance_as_static_method() {
83     assertThat(underTest.getMetadata()).containsSame(FPOrAcceptedNotificationHandler.newMetadata());
84   }
85
86   @Test
87   public void verify_fpOrWontFixIssues_notification_dispatcher_key() {
88     NotificationDispatcherMetadata metadata = FPOrAcceptedNotificationHandler.newMetadata();
89
90     assertThat(metadata.getDispatcherKey()).isEqualTo(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY);
91   }
92
93   @Test
94   public void fpOrWontFixIssues_notification_is_disabled_at_global_level() {
95     NotificationDispatcherMetadata metadata = FPOrAcceptedNotificationHandler.newMetadata();
96
97     assertThat(metadata.getProperty(GLOBAL_NOTIFICATION)).isEqualTo("false");
98   }
99
100   @Test
101   public void fpOrWontFixIssues_notification_is_enable_at_project_level() {
102     NotificationDispatcherMetadata metadata = FPOrAcceptedNotificationHandler.newMetadata();
103
104     assertThat(metadata.getProperty(PER_PROJECT_NOTIFICATION)).isEqualTo("true");
105   }
106
107   @Test
108   public void getNotificationClass_is_IssueChangeNotification() {
109     assertThat(underTest.getNotificationClass()).isEqualTo(IssuesChangesNotification.class);
110   }
111
112   @Test
113   public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
114     when(emailNotificationChannel.isActivated()).thenReturn(false);
115     Set<IssuesChangesNotification> notifications = IntStream.range(0, 5)
116       .mapToObj(i -> mock(IssuesChangesNotification.class))
117       .collect(toSet());
118
119     int deliver = underTest.deliver(notifications);
120
121     assertThat(deliver).isZero();
122     verifyNoInteractions(notificationManager);
123     verify(emailNotificationChannel).isActivated();
124     verifyNoMoreInteractions(emailNotificationChannel);
125     notifications.forEach(Mockito::verifyNoInteractions);
126   }
127
128   @Test
129   public void deliver_parses_every_notification_in_order() {
130     Set<IssuesChangesNotification> notifications = IntStream.range(0, 10)
131       .mapToObj(i -> mock(IssuesChangesNotification.class))
132       .collect(toSet());
133     when(emailNotificationChannel.isActivated()).thenReturn(true);
134     when(serializerMock.from(any(IssuesChangesNotification.class))).thenReturn(mock(IssuesChangesNotificationBuilder.class));
135     FPOrAcceptedNotificationHandler underTest = new FPOrAcceptedNotificationHandler(notificationManager, emailNotificationChannel, serializerMock);
136
137     underTest.deliver(notifications);
138
139     notifications.forEach(notification -> verify(serializerMock).from(notification));
140   }
141
142   @Test
143   public void deliver_fails_with_IAE_if_serializer_throws_IAE() {
144     Set<IssuesChangesNotification> notifications = IntStream.range(0, 10)
145       .mapToObj(i -> mock(IssuesChangesNotification.class))
146       .collect(toSet());
147     when(emailNotificationChannel.isActivated()).thenReturn(true);
148     IllegalArgumentException expected = new IllegalArgumentException("faking serializer#from throwing a IllegalArgumentException");
149     when(serializerMock.from(any(IssuesChangesNotification.class)))
150       .thenReturn(mock(IssuesChangesNotificationBuilder.class))
151       .thenReturn(mock(IssuesChangesNotificationBuilder.class))
152       .thenThrow(expected);
153     FPOrAcceptedNotificationHandler underTest = new FPOrAcceptedNotificationHandler(notificationManager, emailNotificationChannel, serializerMock);
154
155     try {
156       underTest.deliver(notifications);
157       fail("should have throws IAE");
158     } catch (IllegalArgumentException e) {
159       verify(serializerMock, times(3)).from(any(IssuesChangesNotification.class));
160       assertThat(e).isSameAs(expected);
161     }
162   }
163
164   @Test
165   @UseDataProvider("notFPorAcceptedIssueStatus")
166   public void deliver_has_no_effect_if_no_issue_has_FP_or_wontfix_resolution(IssueStatus newIssueStatus) {
167     when(emailNotificationChannel.isActivated()).thenReturn(true);
168     Change changeMock = mock(Change.class);
169     Set<IssuesChangesNotification> notifications = IntStream.range(0, 10)
170       .mapToObj(j -> new IssuesChangesNotificationBuilder(streamOfIssues(t -> t.setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()), changeMock))
171       .map(serializer::serialize)
172       .collect(toSet());
173     reset(serializer);
174
175     int deliver = underTest.deliver(notifications);
176
177     assertThat(deliver).isZero();
178     verify(serializer, times(notifications.size())).from(any(IssuesChangesNotification.class));
179     verifyNoInteractions(changeMock);
180     verifyNoMoreInteractions(serializer);
181     verifyNoInteractions(notificationManager);
182     verify(emailNotificationChannel).isActivated();
183     verifyNoMoreInteractions(emailNotificationChannel);
184   }
185
186   @Test
187   @UseDataProvider("FPorWontFixResolutionWithCorrespondingIssueStatus")
188   public void deliver_shouldNotSendNotification_WhenIssueStatusHasNotChanged(String newResolution,
189     IssueStatus newIssueStatus) {
190     when(emailNotificationChannel.isActivated()).thenReturn(true);
191     Change changeMock = mock(Change.class);
192     Set<IssuesChangesNotification> notifications = IntStream.range(0, 5)
193       .mapToObj(j -> new IssuesChangesNotificationBuilder(streamOfIssues(t -> t.setNewIssueStatus(newIssueStatus).setOldIssueStatus(newIssueStatus)).collect(toSet()), changeMock))
194       .map(serializer::serialize)
195       .collect(toSet());
196     reset(serializer);
197
198     int deliver = underTest.deliver(notifications);
199
200     assertThat(deliver).isZero();
201     verify(serializer, times(notifications.size())).from(any(IssuesChangesNotification.class));
202     verifyNoInteractions(changeMock);
203     verifyNoMoreInteractions(serializer);
204     verifyNoInteractions(notificationManager);
205     verify(emailNotificationChannel).isActivated();
206     verifyNoMoreInteractions(emailNotificationChannel);
207   }
208
209   @DataProvider
210   public static Object[][] notFPorAcceptedIssueStatus() {
211     return new Object[][] {
212       {null},
213       {IssueStatus.FIXED},
214       {IssueStatus.OPEN}
215     };
216   }
217
218   @Test
219   @UseDataProvider("FPorWontFixResolutionWithCorrespondingIssueStatus")
220   public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_FPorWontFix_notifications(String newResolution,
221     IssueStatus newIssueStatus) {
222     Project projectKey1 = newProject(randomAlphabetic(4));
223     Project projectKey2 = newProject(randomAlphabetic(5));
224     Project projectKey3 = newProject(randomAlphabetic(6));
225     Project projectKey4 = newProject(randomAlphabetic(7));
226     Change changeMock = mock(Change.class);
227     // some notifications with some issues on project1
228     Stream<IssuesChangesNotificationBuilder> project1Notifications = IntStream.range(0, 5)
229       .mapToObj(j -> new IssuesChangesNotificationBuilder(
230         streamOfIssues(t -> t.setProject(projectKey1).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()),
231         changeMock));
232     // some notifications with some issues on project2
233     Stream<IssuesChangesNotificationBuilder> project2Notifications = IntStream.range(0, 5)
234       .mapToObj(j -> new IssuesChangesNotificationBuilder(
235         streamOfIssues(t -> t.setProject(projectKey2).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()),
236         changeMock));
237     // some notifications with some issues on project3 and project 4
238     Stream<IssuesChangesNotificationBuilder> project3And4Notifications = IntStream.range(0, 5)
239       .mapToObj(j -> new IssuesChangesNotificationBuilder(
240         Stream.concat(
241             streamOfIssues(t -> t.setProject(projectKey3).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)),
242             streamOfIssues(t -> t.setProject(projectKey4).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)))
243           .collect(toSet()),
244         changeMock));
245     when(emailNotificationChannel.isActivated()).thenReturn(true);
246
247     Set<IssuesChangesNotification> notifications = Stream.of(project1Notifications, project2Notifications, project3And4Notifications)
248       .flatMap(t -> t)
249       .map(serializer::serialize)
250       .collect(toSet());
251     int deliver = underTest.deliver(notifications);
252
253     assertThat(deliver).isZero();
254     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey1.getKey(), ALL_MUST_HAVE_ROLE_USER);
255     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey2.getKey(), ALL_MUST_HAVE_ROLE_USER);
256     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey3.getKey(), ALL_MUST_HAVE_ROLE_USER);
257     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey4.getKey(), ALL_MUST_HAVE_ROLE_USER);
258     verifyNoMoreInteractions(notificationManager);
259     verify(emailNotificationChannel).isActivated();
260     verifyNoMoreInteractions(emailNotificationChannel);
261     verifyNoInteractions(changeMock);
262   }
263
264   @Test
265   @UseDataProvider("FPorWontFixResolutionWithCorrespondingIssueStatus")
266   public void deliver_does_not_send_email_request_for_notifications_a_subscriber_is_the_changeAuthor_of(String newResolution, IssueStatus newIssueStatus) {
267     Project project = newProject(randomAlphabetic(5));
268     User subscriber1 = newUser("subscriber1");
269     User subscriber2 = newUser("subscriber2");
270     User subscriber3 = newUser("subscriber3");
271     User otherChangeAuthor = newUser("otherChangeAuthor");
272
273     // subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 only
274     Set<IssuesChangesNotificationBuilder> subscriber1Notifications = IntStream.range(0, 5)
275       .mapToObj(j -> new IssuesChangesNotificationBuilder(
276         streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)).collect(toSet()),
277         newUserChange(subscriber1)))
278       .collect(toSet());
279     // subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 and subscriber2
280     Set<IssuesChangesNotificationBuilder> subscriber1and2Notifications = IntStream.range(0, 5)
281       .mapToObj(j -> new IssuesChangesNotificationBuilder(
282         Stream.concat(
283             streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)),
284             streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber1)))
285           .collect(toSet()),
286         newUserChange(subscriber1)))
287       .collect(toSet());
288     // subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 only
289     Set<IssuesChangesNotificationBuilder> subscriber2Notifications = IntStream.range(0, 5)
290       .mapToObj(j -> new IssuesChangesNotificationBuilder(
291         streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)).collect(toSet()),
292         newUserChange(subscriber2)))
293       .collect(toSet());
294     // subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 and subscriber 3
295     Set<IssuesChangesNotificationBuilder> subscriber2And3Notifications = IntStream.range(0, 5)
296       .mapToObj(j -> new IssuesChangesNotificationBuilder(
297         Stream.concat(
298             streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber2)),
299             streamOfIssues(t -> t.setProject(project).setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber3)))
300           .collect(toSet()),
301         newUserChange(subscriber2)))
302       .collect(toSet());
303     // subscriber3 is the changeAuthor of no notification
304     // otherChangeAuthor has some notifications
305     Set<IssuesChangesNotificationBuilder> otherChangeAuthorNotifications = IntStream.range(0, 5)
306       .mapToObj(j -> new IssuesChangesNotificationBuilder(streamOfIssues(t -> t.setProject(project)
307         .setNewIssueStatus(newIssueStatus).setOldIssueStatus(IssueStatus.OPEN)).collect(toSet()),
308         newUserChange(otherChangeAuthor)))
309       .collect(toSet());
310     when(emailNotificationChannel.isActivated()).thenReturn(true);
311
312     Set<String> subscriberLogins = ImmutableSet.of(subscriber1.getLogin(), subscriber2.getLogin(), subscriber3.getLogin());
313     when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
314       .thenReturn(subscriberLogins.stream().map(FPOrAcceptedNotificationHandlerTest::emailRecipientOf).collect(toSet()));
315
316     int deliveredCount = 200;
317     when(emailNotificationChannel.deliverAll(anySet()))
318       .thenReturn(deliveredCount)
319       .thenThrow(new IllegalStateException("deliver should be called only once"));
320
321     Set<IssuesChangesNotification> notifications = Stream.of(
322       subscriber1Notifications.stream(),
323       subscriber1and2Notifications.stream(),
324       subscriber2Notifications.stream(),
325       subscriber2And3Notifications.stream(),
326       otherChangeAuthorNotifications.stream())
327       .flatMap(t -> t)
328       .map(serializer::serialize)
329       .collect(toSet());
330     reset(serializer);
331
332     int deliver = underTest.deliver(notifications);
333
334     assertThat(deliver).isEqualTo(deliveredCount);
335     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER);
336     verifyNoMoreInteractions(notificationManager);
337     verify(emailNotificationChannel).isActivated();
338     ArgumentCaptor<Set<EmailDeliveryRequest>> captor = ArgumentCaptor.forClass(requestSetType);
339     verify(emailNotificationChannel).deliverAll(captor.capture());
340     verifyNoMoreInteractions(emailNotificationChannel);
341     ListMultimap<String, EmailDeliveryRequest> requestsByRecipientEmail = captor.getValue().stream()
342       .collect(index(EmailDeliveryRequest::recipientEmail));
343     assertThat(requestsByRecipientEmail.get(emailOf(subscriber1.getLogin())))
344       .containsOnly(
345         Stream.of(
346           subscriber2Notifications.stream()
347             .map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrAccepted(newResolution))),
348           subscriber2And3Notifications.stream()
349             .map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrAccepted(newResolution))),
350           otherChangeAuthorNotifications.stream()
351             .map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrAccepted(newResolution))))
352           .flatMap(t -> t)
353           .toArray(EmailDeliveryRequest[]::new));
354     assertThat(requestsByRecipientEmail.get(emailOf(subscriber2.getLogin())))
355       .containsOnly(
356         Stream.of(
357           subscriber1Notifications.stream()
358             .map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrAccepted(newResolution))),
359           subscriber1and2Notifications.stream()
360             .map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrAccepted(newResolution))),
361           otherChangeAuthorNotifications.stream()
362             .map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrAccepted(newResolution))))
363           .flatMap(t -> t)
364           .toArray(EmailDeliveryRequest[]::new));
365     assertThat(requestsByRecipientEmail.get(emailOf(subscriber3.getLogin())))
366       .containsOnly(
367         Stream.of(
368           subscriber1Notifications.stream()
369             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrAccepted(newResolution))),
370           subscriber1and2Notifications.stream()
371             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrAccepted(newResolution))),
372           subscriber2Notifications.stream()
373             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrAccepted(newResolution))),
374           subscriber2And3Notifications.stream()
375             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrAccepted(newResolution))),
376           otherChangeAuthorNotifications.stream()
377             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrAccepted(newResolution))))
378           .flatMap(t -> t)
379           .toArray(EmailDeliveryRequest[]::new));
380     assertThat(requestsByRecipientEmail.get(emailOf(otherChangeAuthor.getLogin())))
381       .isEmpty();
382   }
383
384   @Test
385   @UseDataProvider("oneOrMoreProjectCounts")
386   public void deliver_send_a_separated_email_request_for_FPs_and_Wont_Fix_issues(int projectCount) {
387     Set<Project> projects = IntStream.range(0, projectCount).mapToObj(i -> newProject("prk_key_" + i)).collect(toSet());
388     User subscriber1 = newUser("subscriber1");
389     User changeAuthor = newUser("changeAuthor");
390
391     Set<ChangedIssue> fpIssues = projects.stream()
392       .flatMap(project -> streamOfIssues(t -> t.setProject(project)
393         .setNewIssueStatus(IssueStatus.FALSE_POSITIVE).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber1)))
394       .collect(toSet());
395     Set<ChangedIssue> wontFixIssues = projects.stream()
396       .flatMap(project -> streamOfIssues(t -> t.setProject(project)
397         .setNewIssueStatus(IssueStatus.ACCEPTED).setOldIssueStatus(IssueStatus.OPEN).setAssignee(subscriber1)))
398       .collect(toSet());
399     UserChange userChange = newUserChange(changeAuthor);
400     IssuesChangesNotificationBuilder fpAndWontFixNotifications = new IssuesChangesNotificationBuilder(
401       Stream.concat(fpIssues.stream(), wontFixIssues.stream()).collect(toSet()),
402       userChange);
403     when(emailNotificationChannel.isActivated()).thenReturn(true);
404     projects.forEach(project -> when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
405       .thenReturn(singleton(emailRecipientOf(subscriber1.getLogin()))));
406
407     int deliveredCount = 200;
408     when(emailNotificationChannel.deliverAll(anySet()))
409       .thenReturn(deliveredCount)
410       .thenThrow(new IllegalStateException("deliver should be called only once"));
411     Set<IssuesChangesNotification> notifications = singleton(serializer.serialize(fpAndWontFixNotifications));
412     reset(serializer);
413
414     int deliver = underTest.deliver(notifications);
415
416      assertThat(deliver).isEqualTo(deliveredCount);
417     projects
418       .forEach(project -> verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER));
419     verifyNoMoreInteractions(notificationManager);
420     verify(emailNotificationChannel).isActivated();
421     ArgumentCaptor<Set<EmailDeliveryRequest>> captor = ArgumentCaptor.forClass(requestSetType);
422     verify(emailNotificationChannel).deliverAll(captor.capture());
423     verifyNoMoreInteractions(emailNotificationChannel);
424     ListMultimap<String, EmailDeliveryRequest> requestsByRecipientEmail = captor.getValue().stream()
425       .collect(index(EmailDeliveryRequest::recipientEmail));
426     assertThat(requestsByRecipientEmail.get(emailOf(subscriber1.getLogin())))
427       .containsOnly(
428         new EmailDeliveryRequest(emailOf(subscriber1.getLogin()), new FPOrAcceptedNotification(
429           userChange, wontFixIssues, FpPrAccepted.ACCEPTED)),
430         new EmailDeliveryRequest(emailOf(subscriber1.getLogin()), new FPOrAcceptedNotification(
431           userChange, fpIssues, FpPrAccepted.FP)));
432   }
433
434   @DataProvider
435   public static Object[][] oneOrMoreProjectCounts() {
436     return new Object[][] {
437       {1},
438       {5},
439     };
440   }
441
442   private static EmailDeliveryRequest newEmailDeliveryRequest(IssuesChangesNotificationBuilder notif, User user, FpPrAccepted resolution) {
443     return new EmailDeliveryRequest(
444       emailOf(user.getLogin()),
445       new FPOrAcceptedNotification(notif.getChange(), notif.getIssues(), resolution));
446   }
447
448   private static FpPrAccepted toFpOrAccepted(String newResolution) {
449     if (newResolution.equals(Issue.RESOLUTION_WONT_FIX)) {
450       return FpPrAccepted.ACCEPTED;
451     }
452     if (newResolution.equals(RESOLUTION_FALSE_POSITIVE)) {
453       return FpPrAccepted.FP;
454     }
455     throw new IllegalArgumentException("unsupported resolution " + newResolution);
456   }
457
458   private static long counter = 233_343;
459
460   private static UserChange newUserChange(User subscriber1) {
461     return new UserChange(counter += 100, subscriber1);
462   }
463
464   public User newUser(String subscriber1) {
465     return new User(subscriber1, subscriber1 + "_login", subscriber1 + "_name");
466   }
467
468   @DataProvider
469   public static Object[][] FPorWontFixResolutionWithCorrespondingIssueStatus() {
470     return new Object[][] {
471       {RESOLUTION_FALSE_POSITIVE, IssueStatus.FALSE_POSITIVE},
472       {Issue.RESOLUTION_WONT_FIX, IssueStatus.ACCEPTED}
473     };
474   }
475
476   private static Stream<ChangedIssue> streamOfIssues(Consumer<ChangedIssue.Builder> consumer) {
477     return IntStream.range(0, 5)
478       .mapToObj(i -> {
479         ChangedIssue.Builder builder = new ChangedIssue.Builder("key_" + i)
480           .setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5)))
481           .setNewStatus(randomAlphabetic(12))
482           .setRule(newRandomNotAHotspotRule(randomAlphabetic(8)))
483           .setProject(new Project.Builder(randomAlphabetic(9))
484             .setKey(randomAlphabetic(10))
485             .setProjectName(randomAlphabetic(11))
486             .build());
487         consumer.accept(builder);
488         return builder.build();
489       });
490   }
491
492   private static NotificationManager.EmailRecipient emailRecipientOf(String assignee1) {
493     return new NotificationManager.EmailRecipient(assignee1, emailOf(assignee1));
494   }
495
496   private static String emailOf(String assignee1) {
497     return assignee1 + "@baffe";
498   }
499
500 }