3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
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.
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.
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.
20 package org.sonar.server.issue.notification;
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.Collections;
28 import java.util.List;
29 import java.util.Random;
31 import java.util.stream.Collectors;
32 import java.util.stream.IntStream;
33 import java.util.stream.Stream;
34 import javax.annotation.Nullable;
35 import org.junit.Test;
36 import org.junit.runner.RunWith;
37 import org.mockito.ArgumentCaptor;
38 import org.mockito.Mockito;
39 import org.sonar.core.util.stream.MoreCollectors;
40 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange;
41 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change;
42 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue;
43 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project;
44 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule;
45 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User;
46 import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange;
47 import org.sonar.server.notification.NotificationDispatcherMetadata;
48 import org.sonar.server.notification.NotificationManager;
49 import org.sonar.server.notification.email.EmailNotificationChannel;
50 import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
52 import static java.util.stream.Collectors.toSet;
53 import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
54 import static org.assertj.core.api.Assertions.assertThat;
55 import static org.mockito.ArgumentMatchers.anySet;
56 import static org.mockito.Mockito.mock;
57 import static org.mockito.Mockito.verify;
58 import static org.mockito.Mockito.verifyNoInteractions;
59 import static org.mockito.Mockito.verifyNoMoreInteractions;
60 import static org.mockito.Mockito.when;
61 import static org.sonar.core.util.stream.MoreCollectors.index;
62 import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex;
63 import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRandomNotAHotspotRule;
64 import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
65 import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
66 import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;
68 @RunWith(DataProviderRunner.class)
69 public class ChangesOnMyIssueNotificationHandlerTest {
70 private static final String CHANGE_ON_MY_ISSUES_DISPATCHER_KEY = "ChangesOnMyIssue";
71 private static final String NO_CHANGE_AUTHOR = null;
73 private NotificationManager notificationManager = mock(NotificationManager.class);
74 private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
75 private IssuesChangesNotificationSerializer serializer = new IssuesChangesNotificationSerializer();
76 private ChangesOnMyIssueNotificationHandler underTest = new ChangesOnMyIssueNotificationHandler(
77 notificationManager, emailNotificationChannel, serializer);
79 private Class<Set<EmailDeliveryRequest>> emailDeliveryRequestSetType = (Class<Set<EmailDeliveryRequest>>) (Object) Set.class;
80 private ArgumentCaptor<Set<EmailDeliveryRequest>> emailDeliveryRequestSetCaptor = ArgumentCaptor.forClass(emailDeliveryRequestSetType);
83 public void getMetadata_returns_same_instance_as_static_method() {
84 assertThat(underTest.getMetadata()).containsSame(ChangesOnMyIssueNotificationHandler.newMetadata());
88 public void verify_changeOnMyIssues_notification_dispatcher_key() {
89 NotificationDispatcherMetadata metadata = ChangesOnMyIssueNotificationHandler.newMetadata();
91 assertThat(metadata.getDispatcherKey()).isEqualTo(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY);
95 public void changeOnMyIssues_notification_is_enable_at_global_level() {
96 NotificationDispatcherMetadata metadata = ChangesOnMyIssueNotificationHandler.newMetadata();
98 assertThat(metadata.getProperty(GLOBAL_NOTIFICATION)).isEqualTo("true");
102 public void changeOnMyIssues_notification_is_enable_at_project_level() {
103 NotificationDispatcherMetadata metadata = ChangesOnMyIssueNotificationHandler.newMetadata();
105 assertThat(metadata.getProperty(PER_PROJECT_NOTIFICATION)).isEqualTo("true");
109 public void getNotificationClass_is_IssueChangeNotification() {
110 assertThat(underTest.getNotificationClass()).isEqualTo(IssuesChangesNotification.class);
114 public void deliver_has_no_effect_if_notifications_is_empty() {
115 when(emailNotificationChannel.isActivated()).thenReturn(true);
116 int deliver = underTest.deliver(Collections.emptyList());
118 assertThat(deliver).isZero();
119 verifyNoInteractions(notificationManager, emailNotificationChannel);
123 public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
124 when(emailNotificationChannel.isActivated()).thenReturn(false);
125 Set<IssuesChangesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
126 .mapToObj(i -> mock(IssuesChangesNotification.class))
129 int deliver = underTest.deliver(notifications);
131 assertThat(deliver).isZero();
132 verifyNoInteractions(notificationManager);
133 verify(emailNotificationChannel).isActivated();
134 verifyNoMoreInteractions(emailNotificationChannel);
135 notifications.forEach(Mockito::verifyNoInteractions);
139 public void deliver_has_no_effect_if_no_notification_has_assignee() {
140 when(emailNotificationChannel.isActivated()).thenReturn(true);
141 Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2))
142 .mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i)
146 .setProject(newProject(i + ""))
149 IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder(issues, new UserChange(new Random().nextLong(), new User("user_uuid", "user_login", null)));
151 int deliver = underTest.deliver(ImmutableSet.of(serializer.serialize(builder)));
153 assertThat(deliver).isZero();
154 verifyNoInteractions(notificationManager);
155 verify(emailNotificationChannel).isActivated();
156 verifyNoMoreInteractions(emailNotificationChannel);
160 public void deliver_has_no_effect_if_all_issues_are_assigned_to_the_changeAuthor() {
161 when(emailNotificationChannel.isActivated()).thenReturn(true);
162 Set<UserChange> userChanges = IntStream.range(0, 1 + new Random().nextInt(3))
163 .mapToObj(i -> new UserChange(new Random().nextLong(), new User("user_uuid_" + i, "user_login_" + i, null)))
165 Set<IssuesChangesNotificationBuilder> notificationBuilders = userChanges.stream()
167 Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2))
168 .mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i + userChange.getUser().getUuid())
170 .setAssignee(userChange.getUser())
172 .setProject(newProject(i + ""))
175 return new IssuesChangesNotificationBuilder(issues, userChange);
178 Set<IssuesChangesNotification> notifications = notificationBuilders.stream()
179 .map(t -> serializer.serialize(t))
182 int deliver = underTest.deliver(notifications);
184 assertThat(deliver).isZero();
185 verifyNoInteractions(notificationManager);
186 verify(emailNotificationChannel).isActivated();
187 verifyNoMoreInteractions(emailNotificationChannel);
191 public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_ChangesOnMyIssues_notifications() {
192 when(emailNotificationChannel.isActivated()).thenReturn(true);
193 Project project = newProject();
194 Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2))
195 .mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i)
197 .setAssignee(newUser("assignee_" + i))
202 IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder(issues, new UserChange(new Random().nextLong(), new User("user_uuid", "user_login", null)));
204 int deliver = underTest.deliver(ImmutableSet.of(serializer.serialize(builder)));
206 assertThat(deliver).isZero();
207 Set<String> assigneeLogins = issues.stream().map(i -> i.getAssignee().get().getLogin()).collect(toSet());
208 verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project.getKey(), assigneeLogins, ALL_MUST_HAVE_ROLE_USER);
209 verifyNoMoreInteractions(notificationManager);
210 verify(emailNotificationChannel).isActivated();
211 verifyNoMoreInteractions(emailNotificationChannel);
215 public void deliver_checks_by_projectKeys_if_notifications_have_subscribed_assignee_to_ChangesOnMyIssues_notifications() {
216 when(emailNotificationChannel.isActivated()).thenReturn(true);
217 Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2))
218 .mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i)
220 .setAssignee(newUser("" + i))
222 .setProject(newProject(i + ""))
225 IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder(issues, new UserChange(new Random().nextLong(), new User("user_uuid", "user_login", null)));
227 int deliver = underTest.deliver(ImmutableSet.of(serializer.serialize(builder)));
229 assertThat(deliver).isZero();
231 .collect(MoreCollectors.index(ChangedIssue::getProject))
233 .forEach((key, value) -> {
234 String projectKey = key.getKey();
235 Set<String> assigneeLogins = value.stream().map(i -> i.getAssignee().get().getLogin()).collect(toSet());
236 verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, assigneeLogins, ALL_MUST_HAVE_ROLE_USER);
238 verifyNoMoreInteractions(notificationManager);
239 verify(emailNotificationChannel).isActivated();
240 verifyNoMoreInteractions(emailNotificationChannel);
244 @UseDataProvider("userOrAnalysisChange")
245 public void deliver_creates_a_notification_per_assignee_with_only_his_issues_on_the_single_project(Change userOrAnalysisChange) {
246 when(emailNotificationChannel.isActivated()).thenReturn(true);
247 Project project = newProject();
248 User assignee1 = newUser("assignee_1");
249 User assignee2 = newUser("assignee_2");
250 Set<ChangedIssue> assignee1Issues = IntStream.range(0, 10)
251 .mapToObj(i -> newChangedIssue("1_issue_key_" + i, assignee1, project))
253 Set<ChangedIssue> assignee2Issues = IntStream.range(0, 10)
254 .mapToObj(i -> newChangedIssue("2_issue_key_" + i, assignee2, project))
256 Set<IssuesChangesNotification> notifications = Stream.of(
257 // notification with only assignee1 5 notifications
258 new IssuesChangesNotificationBuilder(assignee1Issues.stream().limit(5).collect(toSet()), userOrAnalysisChange),
259 // notification with only assignee2 6 notifications
260 new IssuesChangesNotificationBuilder(assignee2Issues.stream().limit(6).collect(toSet()), userOrAnalysisChange),
261 // notification with 4 assignee1 and 3 assignee2 notifications
262 new IssuesChangesNotificationBuilder(
263 Stream.concat(assignee1Issues.stream().skip(6), assignee2Issues.stream().skip(7)).collect(toSet()),
264 userOrAnalysisChange))
265 .map(t -> serializer.serialize(t))
267 when(notificationManager.findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project.getKey(), ImmutableSet.of(assignee1.getLogin(), assignee2.getLogin()),
268 ALL_MUST_HAVE_ROLE_USER))
269 .thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin()), emailRecipientOf(assignee2.getLogin())));
270 int deliveredCount = new Random().nextInt(100);
271 when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount);
273 int deliver = underTest.deliver(notifications);
275 assertThat(deliver).isEqualTo(deliveredCount);
276 verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY,
277 project.getKey(), ImmutableSet.of(assignee1.getLogin(), assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER);
278 verifyNoMoreInteractions(notificationManager);
279 verify(emailNotificationChannel).isActivated();
280 verify(emailNotificationChannel).deliverAll(emailDeliveryRequestSetCaptor.capture());
281 verifyNoMoreInteractions(emailNotificationChannel);
283 Set<EmailDeliveryRequest> emailDeliveryRequests = emailDeliveryRequestSetCaptor.getValue();
284 assertThat(emailDeliveryRequests).hasSize(4);
285 ListMultimap<String, EmailDeliveryRequest> emailDeliveryRequestByEmail = emailDeliveryRequests.stream()
286 .collect(index(EmailDeliveryRequest::recipientEmail));
287 List<EmailDeliveryRequest> assignee1Requests = emailDeliveryRequestByEmail.get(emailOf(assignee1.getLogin()));
288 assertThat(assignee1Requests)
290 .extracting(t -> (ChangesOnMyIssuesNotification) t.notification())
291 .extracting(ChangesOnMyIssuesNotification::getChange)
292 .containsOnly(userOrAnalysisChange);
293 assertThat(assignee1Requests)
294 .extracting(t -> (ChangesOnMyIssuesNotification) t.notification())
295 .extracting(ChangesOnMyIssuesNotification::getChangedIssues)
297 assignee1Issues.stream().limit(5).collect(unorderedIndex(t -> project, t -> t)),
298 assignee1Issues.stream().skip(6).collect(unorderedIndex(t -> project, t -> t)));
300 List<EmailDeliveryRequest> assignee2Requests = emailDeliveryRequestByEmail.get(emailOf(assignee2.getLogin()));
301 assertThat(assignee2Requests)
303 .extracting(t -> (ChangesOnMyIssuesNotification) t.notification())
304 .extracting(ChangesOnMyIssuesNotification::getChange)
305 .containsOnly(userOrAnalysisChange);
306 assertThat(assignee2Requests)
307 .extracting(t -> (ChangesOnMyIssuesNotification) t.notification())
308 .extracting(ChangesOnMyIssuesNotification::getChangedIssues)
310 assignee2Issues.stream().limit(6).collect(unorderedIndex(t -> project, t -> t)),
311 assignee2Issues.stream().skip(7).collect(unorderedIndex(t -> project, t -> t)));
315 @UseDataProvider("userOrAnalysisChange")
316 public void deliver_ignores_issues_which_assignee_is_the_changeAuthor(Change userOrAnalysisChange) {
317 when(emailNotificationChannel.isActivated()).thenReturn(true);
318 Project project1 = newProject();
319 Project project2 = newProject();
320 User assignee1 = newUser("assignee_1");
321 User assignee2 = newUser("assignee_2");
322 Set<ChangedIssue> assignee1Issues = IntStream.range(0, 10)
323 .mapToObj(i -> newChangedIssue("1_issue_key_" + i, assignee1, project1))
325 Set<ChangedIssue> assignee2Issues = IntStream.range(0, 10)
326 .mapToObj(i -> newChangedIssue("2_issue_key_" + i, assignee2, project2))
329 UserChange assignee2Change1 = new UserChange(new Random().nextLong(), assignee2);
330 Set<IssuesChangesNotification> notifications = Stream.of(
331 // notification from assignee1 with issues from assignee1 only
332 new IssuesChangesNotificationBuilder(
333 assignee1Issues.stream().limit(4).collect(toSet()),
334 new UserChange(new Random().nextLong(), assignee1)),
335 // notification from assignee2 with issues from assignee1 and assignee2
336 new IssuesChangesNotificationBuilder(
338 assignee1Issues.stream().skip(4).limit(2),
339 assignee2Issues.stream().limit(4))
342 // notification from assignee2 with issues from assignee2 only
343 new IssuesChangesNotificationBuilder(
344 assignee2Issues.stream().skip(4).limit(3).collect(toSet()),
345 new UserChange(new Random().nextLong(), assignee2)),
346 // notification from other change with issues from assignee1 and assignee2)
347 new IssuesChangesNotificationBuilder(
349 assignee1Issues.stream().skip(6),
350 assignee2Issues.stream().skip(7))
352 userOrAnalysisChange))
353 .map(t -> serializer.serialize(t))
355 when(notificationManager.findSubscribedEmailRecipients(
356 CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project1.getKey(), ImmutableSet.of(assignee1.getLogin()), ALL_MUST_HAVE_ROLE_USER))
357 .thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin())));
358 when(notificationManager.findSubscribedEmailRecipients(
359 CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project2.getKey(), ImmutableSet.of(assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER))
360 .thenReturn(ImmutableSet.of(emailRecipientOf(assignee2.getLogin())));
361 int deliveredCount = new Random().nextInt(100);
362 when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount);
364 int deliver = underTest.deliver(notifications);
366 assertThat(deliver).isEqualTo(deliveredCount);
367 verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY,
368 project1.getKey(), ImmutableSet.of(assignee1.getLogin()), ALL_MUST_HAVE_ROLE_USER);
369 verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY,
370 project2.getKey(), ImmutableSet.of(assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER);
371 verifyNoMoreInteractions(notificationManager);
372 verify(emailNotificationChannel).isActivated();
373 verify(emailNotificationChannel).deliverAll(emailDeliveryRequestSetCaptor.capture());
374 verifyNoMoreInteractions(emailNotificationChannel);
376 Set<EmailDeliveryRequest> emailDeliveryRequests = emailDeliveryRequestSetCaptor.getValue();
377 assertThat(emailDeliveryRequests).hasSize(3);
378 ListMultimap<String, EmailDeliveryRequest> emailDeliveryRequestByEmail = emailDeliveryRequests.stream()
379 .collect(index(EmailDeliveryRequest::recipientEmail));
380 List<EmailDeliveryRequest> assignee1Requests = emailDeliveryRequestByEmail.get(emailOf(assignee1.getLogin()));
381 assertThat(assignee1Requests)
383 .extracting(t -> (ChangesOnMyIssuesNotification) t.notification())
384 .extracting(ChangesOnMyIssuesNotification::getChange)
385 .containsOnly(userOrAnalysisChange, assignee2Change1);
386 assertThat(assignee1Requests)
387 .extracting(t -> (ChangesOnMyIssuesNotification) t.notification())
388 .extracting(ChangesOnMyIssuesNotification::getChangedIssues)
390 assignee1Issues.stream().skip(4).limit(2).collect(unorderedIndex(t -> project1, t -> t)),
391 assignee1Issues.stream().skip(6).collect(unorderedIndex(t -> project1, t -> t)));
393 List<EmailDeliveryRequest> assignee2Requests = emailDeliveryRequestByEmail.get(emailOf(assignee2.getLogin()));
394 assertThat(assignee2Requests)
396 .extracting(t -> (ChangesOnMyIssuesNotification) t.notification())
397 .extracting(ChangesOnMyIssuesNotification::getChange)
398 .containsOnly(userOrAnalysisChange);
399 assertThat(assignee2Requests)
400 .extracting(t -> (ChangesOnMyIssuesNotification) t.notification())
401 .extracting(ChangesOnMyIssuesNotification::getChangedIssues)
402 .containsOnly(assignee2Issues.stream().skip(7).collect(unorderedIndex(t -> project2, t -> t)));
406 public static Object[][] userOrAnalysisChange() {
407 User changeAuthor = new User(randomAlphabetic(12), randomAlphabetic(10), randomAlphabetic(11));
408 return new Object[][] {
409 {new AnalysisChange(new Random().nextLong())},
410 {new UserChange(new Random().nextLong(), changeAuthor)},
414 private static Project newProject() {
415 String base = randomAlphabetic(6);
416 return newProject(base);
419 private static Project newProject(String base) {
420 return new Project.Builder("prj_uuid_" + base)
421 .setKey("prj_key_" + base)
422 .setProjectName("prj_name_" + base)
426 private static User newUser(String name) {
427 return new User(name + "_uuid", name + "login", name);
430 private static ChangedIssue newChangedIssue(String key, User assignee1, Project project) {
431 return new ChangedIssue.Builder(key)
433 .setAssignee(assignee1)
439 private static Rule newRule() {
440 return newRandomNotAHotspotRule(randomAlphabetic(5));
443 private static Set<IssuesChangesNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) {
444 return IntStream.range(0, 1 + new Random().nextInt(5))
445 .mapToObj(i -> newNotification(projectKey, assignee, changeAuthor))
446 .collect(Collectors.toSet());
449 private static IssuesChangesNotification newNotification(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) {
450 return mock(IssuesChangesNotification.class);
453 private static NotificationManager.EmailRecipient emailRecipientOf(String login) {
454 return new NotificationManager.EmailRecipient(login, emailOf(login));
457 private static String emailOf(String login) {
458 return login + "@plouf";