]> source.dussan.org Git - sonarqube.git/blob
318c105cc1af4b494756c3f952b1b459b0fd4f56
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2022 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.Random;
28 import java.util.Set;
29 import java.util.function.Consumer;
30 import java.util.stream.IntStream;
31 import java.util.stream.Stream;
32 import org.junit.Test;
33 import org.junit.runner.RunWith;
34 import org.mockito.ArgumentCaptor;
35 import org.mockito.Mockito;
36 import org.sonar.api.issue.Issue;
37 import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix;
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.lang.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.verifyNoMoreInteractions;
61 import static org.mockito.Mockito.verifyZeroInteractions;
62 import static org.mockito.Mockito.when;
63 import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
64 import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
65 import static org.sonar.core.util.stream.MoreCollectors.index;
66 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject;
67 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
68 import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
69 import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
70 import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
71
72 @RunWith(DataProviderRunner.class)
73 public class FPOrWontFixNotificationHandlerTest {
74   private static final String DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY = "NewFalsePositiveIssue";
75   private NotificationManager notificationManager = mock(NotificationManager.class);
76   private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
77   private IssuesChangesNotificationSerializer serializerMock = mock(IssuesChangesNotificationSerializer.class);
78   private IssuesChangesNotificationSerializer serializer = spy(new IssuesChangesNotificationSerializer());
79   private Class<Set<EmailDeliveryRequest>> requestSetType = (Class<Set<EmailDeliveryRequest>>) (Class<?>) Set.class;
80   private FPOrWontFixNotificationHandler underTest = new FPOrWontFixNotificationHandler(notificationManager, emailNotificationChannel, serializer);
81
82   @Test
83   public void getMetadata_returns_same_instance_as_static_method() {
84     assertThat(underTest.getMetadata().get()).isSameAs(FPOrWontFixNotificationHandler.newMetadata());
85   }
86
87   @Test
88   public void verify_fpOrWontFixIssues_notification_dispatcher_key() {
89     NotificationDispatcherMetadata metadata = FPOrWontFixNotificationHandler.newMetadata();
90
91     assertThat(metadata.getDispatcherKey()).isEqualTo(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY);
92   }
93
94   @Test
95   public void fpOrWontFixIssues_notification_is_disabled_at_global_level() {
96     NotificationDispatcherMetadata metadata = FPOrWontFixNotificationHandler.newMetadata();
97
98     assertThat(metadata.getProperty(GLOBAL_NOTIFICATION)).isEqualTo("false");
99   }
100
101   @Test
102   public void fpOrWontFixIssues_notification_is_enable_at_project_level() {
103     NotificationDispatcherMetadata metadata = FPOrWontFixNotificationHandler.newMetadata();
104
105     assertThat(metadata.getProperty(PER_PROJECT_NOTIFICATION)).isEqualTo("true");
106   }
107
108   @Test
109   public void getNotificationClass_is_IssueChangeNotification() {
110     assertThat(underTest.getNotificationClass()).isEqualTo(IssuesChangesNotification.class);
111   }
112
113   @Test
114   public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
115     when(emailNotificationChannel.isActivated()).thenReturn(false);
116     Set<IssuesChangesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
117       .mapToObj(i -> mock(IssuesChangesNotification.class))
118       .collect(toSet());
119
120     int deliver = underTest.deliver(notifications);
121
122     assertThat(deliver).isZero();
123     verifyZeroInteractions(notificationManager);
124     verify(emailNotificationChannel).isActivated();
125     verifyNoMoreInteractions(emailNotificationChannel);
126     notifications.forEach(Mockito::verifyZeroInteractions);
127   }
128
129   @Test
130   public void deliver_parses_every_notification_in_order() {
131     Set<IssuesChangesNotification> notifications = IntStream.range(0, 5 + new Random().nextInt(10))
132       .mapToObj(i -> mock(IssuesChangesNotification.class))
133       .collect(toSet());
134     when(emailNotificationChannel.isActivated()).thenReturn(true);
135     when(serializerMock.from(any(IssuesChangesNotification.class))).thenReturn(mock(IssuesChangesNotificationBuilder.class));
136     FPOrWontFixNotificationHandler underTest = new FPOrWontFixNotificationHandler(notificationManager, emailNotificationChannel, serializerMock);
137
138     underTest.deliver(notifications);
139
140     notifications.forEach(notification -> verify(serializerMock).from(notification));
141   }
142
143   @Test
144   public void deliver_fails_with_IAE_if_serializer_throws_IAE() {
145     Set<IssuesChangesNotification> notifications = IntStream.range(0, 3 + new Random().nextInt(10))
146       .mapToObj(i -> mock(IssuesChangesNotification.class))
147       .collect(toSet());
148     when(emailNotificationChannel.isActivated()).thenReturn(true);
149     IllegalArgumentException expected = new IllegalArgumentException("faking serializer#from throwing a IllegalArgumentException");
150     when(serializerMock.from(any(IssuesChangesNotification.class)))
151       .thenReturn(mock(IssuesChangesNotificationBuilder.class))
152       .thenReturn(mock(IssuesChangesNotificationBuilder.class))
153       .thenThrow(expected);
154     FPOrWontFixNotificationHandler underTest = new FPOrWontFixNotificationHandler(notificationManager, emailNotificationChannel, serializerMock);
155
156     try {
157       underTest.deliver(notifications);
158       fail("should have throws IAE");
159     } catch (IllegalArgumentException e) {
160       verify(serializerMock, times(3)).from(any(IssuesChangesNotification.class));
161       assertThat(e).isSameAs(expected);
162     }
163   }
164
165   @Test
166   public void deliver_has_no_effect_if_no_issue_has_new_resolution() {
167     when(emailNotificationChannel.isActivated()).thenReturn(true);
168     Change changeMock = mock(Change.class);
169     Set<IssuesChangesNotification> notifications = IntStream.range(0, 2 + new Random().nextInt(5))
170       .mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setNewResolution(null)).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     verifyZeroInteractions(changeMock);
180     verifyNoMoreInteractions(serializer);
181     verifyZeroInteractions(notificationManager);
182     verify(emailNotificationChannel).isActivated();
183     verifyNoMoreInteractions(emailNotificationChannel);
184   }
185
186   @Test
187   @UseDataProvider("notFPorWontFixResolution")
188   public void deliver_has_no_effect_if_no_issue_has_FP_or_wontfix_resolution(String newResolution) {
189     when(emailNotificationChannel.isActivated()).thenReturn(true);
190     Change changeMock = mock(Change.class);
191     Set<IssuesChangesNotification> notifications = IntStream.range(0, 2 + new Random().nextInt(5))
192       .mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setNewResolution(newResolution)).collect(toSet()), changeMock))
193       .map(serializer::serialize)
194       .collect(toSet());
195     reset(serializer);
196
197     int deliver = underTest.deliver(notifications);
198
199     assertThat(deliver).isZero();
200     verify(serializer, times(notifications.size())).from(any(IssuesChangesNotification.class));
201     verifyZeroInteractions(changeMock);
202     verifyNoMoreInteractions(serializer);
203     verifyZeroInteractions(notificationManager);
204     verify(emailNotificationChannel).isActivated();
205     verifyNoMoreInteractions(emailNotificationChannel);
206   }
207
208   @DataProvider
209   public static Object[][] notFPorWontFixResolution() {
210     return new Object[][] {
211       {""},
212       {randomAlphabetic(9)},
213       {Issue.RESOLUTION_FIXED},
214       {Issue.RESOLUTION_REMOVED}
215     };
216   }
217
218   @Test
219   @UseDataProvider("FPorWontFixResolution")
220   public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_FPorWontFix_notifications(String newResolution) {
221     Project projectKey1 = newProject(randomAlphabetic(4));
222     Project projectKey2 = newProject(randomAlphabetic(5));
223     Project projectKey3 = newProject(randomAlphabetic(6));
224     Project projectKey4 = newProject(randomAlphabetic(7));
225     Change changeMock = mock(Change.class);
226     // some notifications with some issues on project1
227     Stream<IssuesChangesNotificationBuilder> project1Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
228       .mapToObj(j -> new IssuesChangesNotificationBuilder(
229         randomIssues(t -> t.setProject(projectKey1).setNewResolution(newResolution)).collect(toSet()),
230         changeMock));
231     // some notifications with some issues on project2
232     Stream<IssuesChangesNotificationBuilder> project2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
233       .mapToObj(j -> new IssuesChangesNotificationBuilder(
234         randomIssues(t -> t.setProject(projectKey2).setNewResolution(newResolution)).collect(toSet()),
235         changeMock));
236     // some notifications with some issues on project3 and project 4
237     Stream<IssuesChangesNotificationBuilder> project3And4Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
238       .mapToObj(j -> new IssuesChangesNotificationBuilder(
239         Stream.concat(
240           randomIssues(t -> t.setProject(projectKey3).setNewResolution(newResolution)),
241           randomIssues(t -> t.setProject(projectKey4).setNewResolution(newResolution)))
242           .collect(toSet()),
243         changeMock));
244     when(emailNotificationChannel.isActivated()).thenReturn(true);
245
246     Set<IssuesChangesNotification> notifications = Stream.of(project1Notifications, project2Notifications, project3And4Notifications)
247       .flatMap(t -> t)
248       .map(serializer::serialize)
249       .collect(toSet());
250     int deliver = underTest.deliver(notifications);
251
252     assertThat(deliver).isZero();
253     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey1.getKey(), ALL_MUST_HAVE_ROLE_USER);
254     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey2.getKey(), ALL_MUST_HAVE_ROLE_USER);
255     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey3.getKey(), ALL_MUST_HAVE_ROLE_USER);
256     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey4.getKey(), ALL_MUST_HAVE_ROLE_USER);
257     verifyNoMoreInteractions(notificationManager);
258     verify(emailNotificationChannel).isActivated();
259     verifyNoMoreInteractions(emailNotificationChannel);
260     verifyZeroInteractions(changeMock);
261   }
262
263   @Test
264   @UseDataProvider("FPorWontFixResolution")
265   public void deliver_does_not_send_email_request_for_notifications_a_subscriber_is_the_changeAuthor_of(String newResolution) {
266     Project project = newProject(randomAlphabetic(5));
267     User subscriber1 = newUser("subscriber1");
268     User subscriber2 = newUser("subscriber2");
269     User subscriber3 = newUser("subscriber3");
270     User otherChangeAuthor = newUser("otherChangeAuthor");
271
272     // subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 only
273     Set<IssuesChangesNotificationBuilder> subscriber1Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
274       .mapToObj(j -> new IssuesChangesNotificationBuilder(
275         randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)).collect(toSet()),
276         newUserChange(subscriber1)))
277       .collect(toSet());
278     // subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 and subscriber2
279     Set<IssuesChangesNotificationBuilder> subscriber1and2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
280       .mapToObj(j -> new IssuesChangesNotificationBuilder(
281         Stream.concat(
282           randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)),
283           randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber1)))
284           .collect(toSet()),
285         newUserChange(subscriber1)))
286       .collect(toSet());
287     // subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 only
288     Set<IssuesChangesNotificationBuilder> subscriber2Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
289       .mapToObj(j -> new IssuesChangesNotificationBuilder(
290         randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)).collect(toSet()),
291         newUserChange(subscriber2)))
292       .collect(toSet());
293     // subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 and subscriber 3
294     Set<IssuesChangesNotificationBuilder> subscriber2And3Notifications = IntStream.range(0, 1 + new Random().nextInt(2))
295       .mapToObj(j -> new IssuesChangesNotificationBuilder(
296         Stream.concat(
297           randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)),
298           randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber3)))
299           .collect(toSet()),
300         newUserChange(subscriber2)))
301       .collect(toSet());
302     // subscriber3 is the changeAuthor of no notification
303     // otherChangeAuthor has some notifications
304     Set<IssuesChangesNotificationBuilder> otherChangeAuthorNotifications = IntStream.range(0, 1 + new Random().nextInt(2))
305       .mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setProject(project).setNewResolution(newResolution)).collect(toSet()),
306         newUserChange(otherChangeAuthor)))
307       .collect(toSet());
308     when(emailNotificationChannel.isActivated()).thenReturn(true);
309
310     Set<String> subscriberLogins = ImmutableSet.of(subscriber1.getLogin(), subscriber2.getLogin(), subscriber3.getLogin());
311     when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
312       .thenReturn(subscriberLogins.stream().map(FPOrWontFixNotificationHandlerTest::emailRecipientOf).collect(toSet()));
313
314     int deliveredCount = new Random().nextInt(200);
315     when(emailNotificationChannel.deliverAll(anySet()))
316       .thenReturn(deliveredCount)
317       .thenThrow(new IllegalStateException("deliver should be called only once"));
318
319     Set<IssuesChangesNotification> notifications = Stream.of(
320       subscriber1Notifications.stream(),
321       subscriber1and2Notifications.stream(),
322       subscriber2Notifications.stream(),
323       subscriber2And3Notifications.stream(),
324       otherChangeAuthorNotifications.stream())
325       .flatMap(t -> t)
326       .map(serializer::serialize)
327       .collect(toSet());
328     reset(serializer);
329
330     int deliver = underTest.deliver(notifications);
331
332     assertThat(deliver).isEqualTo(deliveredCount);
333     verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER);
334     verifyNoMoreInteractions(notificationManager);
335     verify(emailNotificationChannel).isActivated();
336     ArgumentCaptor<Set<EmailDeliveryRequest>> captor = ArgumentCaptor.forClass(requestSetType);
337     verify(emailNotificationChannel).deliverAll(captor.capture());
338     verifyNoMoreInteractions(emailNotificationChannel);
339     ListMultimap<String, EmailDeliveryRequest> requestsByRecipientEmail = captor.getValue().stream()
340       .collect(index(EmailDeliveryRequest::getRecipientEmail));
341     assertThat(requestsByRecipientEmail.get(emailOf(subscriber1.getLogin())))
342       .containsOnly(
343         Stream.of(
344           subscriber2Notifications.stream()
345             .map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrWontFix(newResolution))),
346           subscriber2And3Notifications.stream()
347             .map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrWontFix(newResolution))),
348           otherChangeAuthorNotifications.stream()
349             .map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrWontFix(newResolution))))
350           .flatMap(t -> t)
351           .toArray(EmailDeliveryRequest[]::new));
352     assertThat(requestsByRecipientEmail.get(emailOf(subscriber2.getLogin())))
353       .containsOnly(
354         Stream.of(
355           subscriber1Notifications.stream()
356             .map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrWontFix(newResolution))),
357           subscriber1and2Notifications.stream()
358             .map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrWontFix(newResolution))),
359           otherChangeAuthorNotifications.stream()
360             .map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrWontFix(newResolution))))
361           .flatMap(t -> t)
362           .toArray(EmailDeliveryRequest[]::new));
363     assertThat(requestsByRecipientEmail.get(emailOf(subscriber3.getLogin())))
364       .containsOnly(
365         Stream.of(
366           subscriber1Notifications.stream()
367             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))),
368           subscriber1and2Notifications.stream()
369             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))),
370           subscriber2Notifications.stream()
371             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))),
372           subscriber2And3Notifications.stream()
373             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))),
374           otherChangeAuthorNotifications.stream()
375             .map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))))
376           .flatMap(t -> t)
377           .toArray(EmailDeliveryRequest[]::new));
378     assertThat(requestsByRecipientEmail.get(emailOf(otherChangeAuthor.getLogin())))
379       .isEmpty();
380   }
381
382   @Test
383   @UseDataProvider("oneOrMoreProjectCounts")
384   public void deliver_send_a_separated_email_request_for_FPs_and_Wont_Fix_issues(int projectCount) {
385     Set<Project> projects = IntStream.range(0, projectCount).mapToObj(i -> newProject("prk_key_" + i)).collect(toSet());
386     User subscriber1 = newUser("subscriber1");
387     User changeAuthor = newUser("changeAuthor");
388
389     Set<ChangedIssue> fpIssues = projects.stream()
390       .flatMap(project -> randomIssues(t -> t.setProject(project).setNewResolution(RESOLUTION_FALSE_POSITIVE).setAssignee(subscriber1)))
391       .collect(toSet());
392     Set<ChangedIssue> wontFixIssues = projects.stream()
393       .flatMap(project -> randomIssues(t -> t.setProject(project).setNewResolution(RESOLUTION_WONT_FIX).setAssignee(subscriber1)))
394       .collect(toSet());
395     UserChange userChange = newUserChange(changeAuthor);
396     IssuesChangesNotificationBuilder fpAndWontFixNotifications = new IssuesChangesNotificationBuilder(
397       Stream.concat(fpIssues.stream(), wontFixIssues.stream()).collect(toSet()),
398       userChange);
399     when(emailNotificationChannel.isActivated()).thenReturn(true);
400     projects.forEach(project -> when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER))
401       .thenReturn(singleton(emailRecipientOf(subscriber1.getLogin()))));
402
403     int deliveredCount = new Random().nextInt(200);
404     when(emailNotificationChannel.deliverAll(anySet()))
405       .thenReturn(deliveredCount)
406       .thenThrow(new IllegalStateException("deliver should be called only once"));
407     Set<IssuesChangesNotification> notifications = singleton(serializer.serialize(fpAndWontFixNotifications));
408     reset(serializer);
409
410     int deliver = underTest.deliver(notifications);
411
412     assertThat(deliver).isEqualTo(deliveredCount);
413     projects
414       .forEach(project -> verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER));
415     verifyNoMoreInteractions(notificationManager);
416     verify(emailNotificationChannel).isActivated();
417     ArgumentCaptor<Set<EmailDeliveryRequest>> captor = ArgumentCaptor.forClass(requestSetType);
418     verify(emailNotificationChannel).deliverAll(captor.capture());
419     verifyNoMoreInteractions(emailNotificationChannel);
420     ListMultimap<String, EmailDeliveryRequest> requestsByRecipientEmail = captor.getValue().stream()
421       .collect(index(EmailDeliveryRequest::getRecipientEmail));
422     assertThat(requestsByRecipientEmail.get(emailOf(subscriber1.getLogin())))
423       .containsOnly(
424         new EmailDeliveryRequest(emailOf(subscriber1.getLogin()), new FPOrWontFixNotification(
425           userChange, wontFixIssues, FpOrWontFix.WONT_FIX)),
426         new EmailDeliveryRequest(emailOf(subscriber1.getLogin()), new FPOrWontFixNotification(
427           userChange, fpIssues, FpOrWontFix.FP)));
428   }
429
430   @DataProvider
431   public static Object[][] oneOrMoreProjectCounts() {
432     return new Object[][] {
433       {1},
434       {2 + new Random().nextInt(3)},
435     };
436   }
437
438   private static EmailDeliveryRequest newEmailDeliveryRequest(IssuesChangesNotificationBuilder notif, User user, FpOrWontFix resolution) {
439     return new EmailDeliveryRequest(
440       emailOf(user.getLogin()),
441       new FPOrWontFixNotification(notif.getChange(), notif.getIssues(), resolution));
442   }
443
444   private static FpOrWontFix toFpOrWontFix(String newResolution) {
445     if (newResolution.equals(Issue.RESOLUTION_WONT_FIX)) {
446       return FpOrWontFix.WONT_FIX;
447     }
448     if (newResolution.equals(RESOLUTION_FALSE_POSITIVE)) {
449       return FpOrWontFix.FP;
450     }
451     throw new IllegalArgumentException("unsupported resolution " + newResolution);
452   }
453
454   private static long counter = 233_343;
455
456   private static UserChange newUserChange(User subscriber1) {
457     return new UserChange(counter += 100, subscriber1);
458   }
459
460   public User newUser(String subscriber1) {
461     return new User(subscriber1, subscriber1 + "_login", subscriber1 + "_name");
462   }
463
464   @DataProvider
465   public static Object[][] FPorWontFixResolution() {
466     return new Object[][] {
467       {RESOLUTION_FALSE_POSITIVE},
468       {Issue.RESOLUTION_WONT_FIX}
469     };
470   }
471
472   private static Stream<ChangedIssue> randomIssues(Consumer<ChangedIssue.Builder> consumer) {
473     return IntStream.range(0, 1 + new Random().nextInt(5))
474       .mapToObj(i -> {
475         ChangedIssue.Builder builder = new ChangedIssue.Builder("key_" + i)
476           .setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5)))
477           .setNewStatus(randomAlphabetic(12))
478           .setNewResolution(randomAlphabetic(13))
479           .setRule(newRandomNotAHotspotRule(randomAlphabetic(8)))
480           .setProject(new Project.Builder(randomAlphabetic(9))
481             .setKey(randomAlphabetic(10))
482             .setProjectName(randomAlphabetic(11))
483             .build());
484         consumer.accept(builder);
485         return builder.build();
486       });
487   }
488
489   private static NotificationManager.EmailRecipient emailRecipientOf(String assignee1) {
490     return new NotificationManager.EmailRecipient(assignee1, emailOf(assignee1));
491   }
492
493   private static String emailOf(String assignee1) {
494     return assignee1 + "@baffe";
495   }
496
497 }