From: Simon Brandhof Date: Mon, 5 Sep 2016 20:20:34 +0000 (+0200) Subject: SONAR-7578 do not start notification daemon on CE X-Git-Tag: 6.1-RC1~206 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=ebc43c59ac25a3c75b9ce089f90e9eaac6c74637;p=sonarqube.git SONAR-7578 do not start notification daemon on CE --- diff --git a/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java b/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java index 9c5f6d2c91a..efdd3c28bf6 100644 --- a/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java +++ b/it/it-tests/src/test/java/it/issue/IssueNotificationsTest.java @@ -198,7 +198,7 @@ public class IssueNotificationsTest extends AbstractIssueTest { } private static void waitUntilAllNotificationsAreDelivered() throws InterruptedException { - Thread.sleep(10000); + Thread.sleep(10_000L); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDaemon.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDaemon.java new file mode 100644 index 00000000000..3ac96fc3cb8 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDaemon.java @@ -0,0 +1,137 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.picocontainer.Startable; +import org.sonar.api.Properties; +import org.sonar.api.Property; +import org.sonar.api.config.Settings; +import org.sonar.api.notifications.Notification; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +@Properties({ + @Property( + key = NotificationDaemon.PROPERTY_DELAY, + defaultValue = "60", + name = "Delay of notifications, in seconds", + project = false, + global = false), + @Property( + key = NotificationDaemon.PROPERTY_DELAY_BEFORE_REPORTING_STATUS, + defaultValue = "600", + name = "Delay before reporting notification status, in seconds", + project = false, + global = false) +}) +@ServerSide +public class NotificationDaemon implements Startable { + private static final String THREAD_NAME_PREFIX = "sq-notification-service-"; + + private static final Logger LOG = Loggers.get(NotificationDaemon.class); + + public static final String PROPERTY_DELAY = "sonar.notifications.delay"; + public static final String PROPERTY_DELAY_BEFORE_REPORTING_STATUS = "sonar.notifications.runningDelayBeforeReportingStatus"; + + private final long delayInSeconds; + private final long delayBeforeReportingStatusInSeconds; + private final DefaultNotificationManager manager; + private final NotificationService service; + + private ScheduledExecutorService executorService; + private boolean stopping = false; + + public NotificationDaemon(Settings settings, DefaultNotificationManager manager, NotificationService service) { + this.delayInSeconds = settings.getLong(PROPERTY_DELAY); + this.delayBeforeReportingStatusInSeconds = settings.getLong(PROPERTY_DELAY_BEFORE_REPORTING_STATUS); + this.manager = manager; + this.service = service; + } + + @Override + public void start() { + executorService = Executors.newSingleThreadScheduledExecutor( + new ThreadFactoryBuilder() + .setNameFormat(THREAD_NAME_PREFIX + "%d") + .setPriority(Thread.MIN_PRIORITY) + .build()); + executorService.scheduleWithFixedDelay(() -> { + try { + processQueue(); + } catch (Exception e) { + LOG.error("Error in NotificationService", e); + } + }, 0, delayInSeconds, TimeUnit.SECONDS); + LOG.info("Notification service started (delay {} sec.)", delayInSeconds); + } + + @Override + public void stop() { + try { + stopping = true; + executorService.shutdown(); + executorService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LOG.error("Error during stop of notification service", e); + } + LOG.info("Notification service stopped"); + } + + @VisibleForTesting + synchronized void processQueue() { + long start = now(); + long lastLog = start; + long notifSentCount = 0; + + Notification notifToSend = manager.getFromQueue(); + while (notifToSend != null) { + service.deliver(notifToSend); + notifSentCount++; + if (stopping) { + break; + } + long now = now(); + if (now - lastLog > delayBeforeReportingStatusInSeconds * 1000) { + long remainingNotifCount = manager.count(); + lastLog = now; + long spentTimeInMinutes = (now - start) / (60 * 1000); + log(notifSentCount, remainingNotifCount, spentTimeInMinutes); + } + notifToSend = manager.getFromQueue(); + } + } + + @VisibleForTesting + void log(long notifSentCount, long remainingNotifCount, long spentTimeInMinutes) { + LOG.info("{} notifications sent during the past {} minutes and {} still waiting to be sent", + notifSentCount, spentTimeInMinutes, remainingNotifCount); + } + + @VisibleForTesting + long now() { + return System.currentTimeMillis(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationService.java b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationService.java index 6e5aa86b926..c86451b343a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/notification/NotificationService.java @@ -24,68 +24,30 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; import com.google.common.collect.SetMultimap; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; -import org.picocontainer.Startable; -import org.sonar.api.Properties; -import org.sonar.api.Property; -import org.sonar.api.config.Settings; +import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.notifications.Notification; import org.sonar.api.notifications.NotificationChannel; -import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.db.DbClient; -@Properties({ - @Property( - key = NotificationService.PROPERTY_DELAY, - defaultValue = "60", - name = "Delay of notifications, in seconds", - project = false, - global = false), - @Property( - key = NotificationService.PROPERTY_DELAY_BEFORE_REPORTING_STATUS, - defaultValue = "600", - name = "Delay before reporting notification status, in seconds", - project = false, - global = false) -}) @ServerSide @ComputeEngineSide -public class NotificationService implements Startable { - private static final String THREAD_NAME_PREFIX = "sq-notification-service-"; +public class NotificationService { private static final Logger LOG = Loggers.get(NotificationService.class); - public static final String PROPERTY_DELAY = "sonar.notifications.delay"; - public static final String PROPERTY_DELAY_BEFORE_REPORTING_STATUS = "sonar.notifications.runningDelayBeforeReportingStatus"; - - private final long delayInSeconds; - private final long delayBeforeReportingStatusInSeconds; - private final DefaultNotificationManager manager; private final List dispatchers; private final DbClient dbClient; - private ScheduledExecutorService executorService; - private boolean stopping = false; - private final boolean disabled; - - public NotificationService(Settings settings, DefaultNotificationManager manager, DbClient dbClient, - NotificationDispatcher[] dispatchers) { - this.disabled = "ComputeEngineSettings".equals(settings.getClass().getSimpleName()); - this.delayInSeconds = settings.getLong(PROPERTY_DELAY); - this.delayBeforeReportingStatusInSeconds = settings.getLong(PROPERTY_DELAY_BEFORE_REPORTING_STATUS); - this.manager = manager; + public NotificationService(DbClient dbClient, NotificationDispatcher[] dispatchers) { this.dbClient = dbClient; this.dispatchers = ImmutableList.copyOf(dispatchers); } @@ -93,75 +55,8 @@ public class NotificationService implements Startable { /** * Default constructor when no dispatchers. */ - public NotificationService(Settings settings, DefaultNotificationManager manager, DbClient dbClient) { - this(settings, manager, dbClient, new NotificationDispatcher[0]); - } - - @Override - public void start() { - if (!disabled) { - executorService = - Executors.newSingleThreadScheduledExecutor( - new ThreadFactoryBuilder() - .setNameFormat(THREAD_NAME_PREFIX + "%d") - .setPriority(Thread.MIN_PRIORITY) - .build()); - executorService.scheduleWithFixedDelay(new Runnable() { - @Override - public void run() { - try { - processQueue(); - } catch (Exception e) { - LOG.error("Error in NotificationService", e); - } - } - }, 0, delayInSeconds, TimeUnit.SECONDS); - LOG.info("Notification service started (delay {} sec.)", delayInSeconds); - } - } - - @Override - public void stop() { - if (!disabled) { - try { - stopping = true; - executorService.shutdown(); - executorService.awaitTermination(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - LOG.error("Error during stop of notification service", e); - } - LOG.info("Notification service stopped"); - } - } - - @VisibleForTesting - synchronized void processQueue() { - long start = now(); - long lastLog = start; - long notifSentCount = 0; - - Notification notifToSend = manager.getFromQueue(); - while (notifToSend != null) { - deliver(notifToSend); - notifSentCount++; - if (stopping) { - break; - } - long now = now(); - if (now - lastLog > delayBeforeReportingStatusInSeconds * 1000) { - long remainingNotifCount = manager.count(); - lastLog = now; - long spentTimeInMinutes = (now - start) / (60 * 1000); - log(notifSentCount, remainingNotifCount, spentTimeInMinutes); - } - notifToSend = manager.getFromQueue(); - } - } - - @VisibleForTesting - void log(long notifSentCount, long remainingNotifCount, long spentTimeInMinutes) { - LOG.info("{} notifications sent during the past {} minutes and {} still waiting to be sent", - new Object[] {notifSentCount, spentTimeInMinutes, remainingNotifCount}); + public NotificationService(DbClient dbClient) { + this(dbClient, new NotificationDispatcher[0]); } @VisibleForTesting diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 6b465505031..92c84a23d5c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -135,6 +135,7 @@ import org.sonar.server.metric.DefaultMetricFinder; import org.sonar.server.metric.ws.MetricsWsModule; import org.sonar.server.notification.DefaultNotificationManager; import org.sonar.server.notification.NotificationCenter; +import org.sonar.server.notification.NotificationDaemon; import org.sonar.server.notification.NotificationService; import org.sonar.server.notification.email.AlertsEmailTemplate; import org.sonar.server.notification.email.EmailNotificationChannel; @@ -578,6 +579,7 @@ public class PlatformLevel4 extends PlatformLevel { NotificationCenter.class, DefaultNotificationManager.class, EmailsWsModule.class, + NotificationDaemon.class, // Tests TestsWs.class, diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java new file mode 100644 index 00000000000..9244a69b52b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java @@ -0,0 +1,260 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.notification; + +import com.google.common.collect.Sets; +import java.util.Arrays; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sonar.api.config.Settings; +import org.sonar.api.config.MapSettings; +import org.sonar.api.notifications.Notification; +import org.sonar.api.notifications.NotificationChannel; +import org.sonar.db.DbClient; +import org.sonar.db.property.PropertiesDao; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.same; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class NotificationDaemonTest { + private static String CREATOR_SIMON = "simon"; + private static String CREATOR_EVGENY = "evgeny"; + private static String ASSIGNEE_SIMON = "simon"; + + private DefaultNotificationManager manager = mock(DefaultNotificationManager.class); + private Notification notification = mock(Notification.class); + private NotificationChannel emailChannel = mock(NotificationChannel.class); + private NotificationChannel gtalkChannel = mock(NotificationChannel.class); + private NotificationDispatcher commentOnIssueAssignedToMe = mock(NotificationDispatcher.class); + private NotificationDispatcher commentOnIssueCreatedByMe = mock(NotificationDispatcher.class); + private NotificationDispatcher qualityGateChange = mock(NotificationDispatcher.class); + private DbClient dbClient = mock(DbClient.class); + private NotificationService service = new NotificationService(dbClient, new NotificationDispatcher[]{commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange}); + private NotificationDaemon underTest = null; + + private void setUpMocks() { + when(emailChannel.getKey()).thenReturn("email"); + when(gtalkChannel.getKey()).thenReturn("gtalk"); + when(commentOnIssueAssignedToMe.getKey()).thenReturn("CommentOnIssueAssignedToMe"); + when(commentOnIssueAssignedToMe.getType()).thenReturn("issue-changes"); + when(commentOnIssueCreatedByMe.getKey()).thenReturn("CommentOnIssueCreatedByMe"); + when(commentOnIssueCreatedByMe.getType()).thenReturn("issue-changes"); + when(qualityGateChange.getKey()).thenReturn("QGateChange"); + when(qualityGateChange.getType()).thenReturn("qgate-changes"); + when(manager.getFromQueue()).thenReturn(notification).thenReturn(null); + + Settings settings = new MapSettings().setProperty("sonar.notifications.delay", 1L); + + underTest = new NotificationDaemon(settings, manager, service); + } + + /** + * Given: + * Simon wants to receive notifications by email on comments for reviews assigned to him or created by him. + *

+ * When: + * Freddy adds comment to review created by Simon and assigned to Simon. + *

+ * Then: + * Only one notification should be delivered to Simon by Email. + */ + @Test + public void scenario1() { + setUpMocks(); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + underTest.stop(); + + verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); + } + + /** + * Given: + * Evgeny wants to receive notification by GTalk on comments for reviews created by him. + * Simon wants to receive notification by Email on comments for reviews assigned to him. + *

+ * When: + * Freddy adds comment to review created by Evgeny and assigned to Simon. + *

+ * Then: + * Two notifications should be delivered - one to Simon by Email and another to Evgeny by GTalk. + */ + @Test + public void scenario2() { + setUpMocks(); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_EVGENY, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + verify(gtalkChannel, timeout(2000)).deliver(notification, CREATOR_EVGENY); + underTest.stop(); + + verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); + verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); + } + + /** + * Given: + * Simon wants to receive notifications by Email and GTLak on comments for reviews assigned to him. + *

+ * When: + * Freddy adds comment to review created by Evgeny and assigned to Simon. + *

+ * Then: + * Two notifications should be delivered to Simon - one by Email and another by GTalk. + */ + @Test + public void scenario3() { + setUpMocks(); + doAnswer(addUser(ASSIGNEE_SIMON, new NotificationChannel[]{emailChannel, gtalkChannel})) + .when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + verify(gtalkChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + underTest.stop(); + + verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); + verify(gtalkChannel, never()).deliver(notification, CREATOR_EVGENY); + } + + /** + * Given: + * Nobody wants to receive notifications. + *

+ * When: + * Freddy adds comment to review created by Evgeny and assigned to Simon. + *

+ * Then: + * No notifications. + */ + @Test + public void scenario4() { + setUpMocks(); + + underTest.start(); + underTest.stop(); + + verify(emailChannel, never()).deliver(any(Notification.class), anyString()); + verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); + } + + // SONAR-4548 + @Test + public void shouldNotStopWhenException() { + setUpMocks(); + when(manager.getFromQueue()).thenThrow(new RuntimeException("Unexpected exception")).thenReturn(notification).thenReturn(null); + doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); + underTest.stop(); + + verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); + } + + @Test + public void shouldNotAddNullAsUser() { + setUpMocks(); + doAnswer(addUser(null, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); + + underTest.start(); + underTest.stop(); + + verify(emailChannel, never()).deliver(any(Notification.class), anyString()); + verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); + } + + @Test + public void getDispatchers() { + setUpMocks(); + + assertThat(service.getDispatchers()).containsOnly(commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange); + } + + @Test + public void getDispatchers_empty() { + Settings settings = new MapSettings().setProperty("sonar.notifications.delay", 1L); + + service = new NotificationService(dbClient); + assertThat(service.getDispatchers()).hasSize(0); + } + + @Test + public void shouldLogEvery10Minutes() { + setUpMocks(); + // Emulate 2 notifications in DB + when(manager.getFromQueue()).thenReturn(notification).thenReturn(notification).thenReturn(null); + when(manager.count()).thenReturn(1L).thenReturn(0L); + underTest = spy(underTest); + // Emulate processing of each notification take 10 min to have a log each time + when(underTest.now()).thenReturn(0L).thenReturn(10 * 60 * 1000 + 1L).thenReturn(20 * 60 * 1000 + 2L); + underTest.start(); + verify(underTest, timeout(200)).log(1, 1, 10); + verify(underTest, timeout(200)).log(2, 0, 20); + underTest.stop(); + } + + @Test + public void hasProjectSubscribersForType() { + setUpMocks(); + + PropertiesDao dao = mock(PropertiesDao.class); + when(dbClient.propertiesDao()).thenReturn(dao); + + // no subscribers + when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(false); + assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isFalse(); + + // has subscribers on one dispatcher (among the two) + when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(true); + assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isTrue(); + } + + private static Answer addUser(final String user, final NotificationChannel channel) { + return addUser(user, new NotificationChannel[]{channel}); + } + + private static Answer addUser(final String user, final NotificationChannel[] channels) { + return new Answer() { + public Object answer(InvocationOnMock invocation) { + for (NotificationChannel channel : channels) { + ((NotificationDispatcher.Context) invocation.getArguments()[1]).addUser(user, channel); + } + return null; + } + }; + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationServiceTest.java deleted file mode 100644 index 2df7b3c2915..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationServiceTest.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2016 SonarSource SA - * mailto:contact AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.notification; - -import com.google.common.collect.Sets; -import java.util.Arrays; -import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.sonar.api.config.Settings; -import org.sonar.api.config.MapSettings; -import org.sonar.api.notifications.Notification; -import org.sonar.api.notifications.NotificationChannel; -import org.sonar.db.DbClient; -import org.sonar.db.property.PropertiesDao; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.same; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class NotificationServiceTest { - private static String CREATOR_SIMON = "simon"; - private static String CREATOR_EVGENY = "evgeny"; - private static String ASSIGNEE_SIMON = "simon"; - - DefaultNotificationManager manager = mock(DefaultNotificationManager.class); - Notification notification = mock(Notification.class); - NotificationChannel emailChannel = mock(NotificationChannel.class); - NotificationChannel gtalkChannel = mock(NotificationChannel.class); - NotificationDispatcher commentOnIssueAssignedToMe = mock(NotificationDispatcher.class); - NotificationDispatcher commentOnIssueCreatedByMe = mock(NotificationDispatcher.class); - NotificationDispatcher qualityGateChange = mock(NotificationDispatcher.class); - DbClient dbClient = mock(DbClient.class); - - private NotificationService service; - - private void setUpMocks() { - when(emailChannel.getKey()).thenReturn("email"); - when(gtalkChannel.getKey()).thenReturn("gtalk"); - when(commentOnIssueAssignedToMe.getKey()).thenReturn("CommentOnIssueAssignedToMe"); - when(commentOnIssueAssignedToMe.getType()).thenReturn("issue-changes"); - when(commentOnIssueCreatedByMe.getKey()).thenReturn("CommentOnIssueCreatedByMe"); - when(commentOnIssueCreatedByMe.getType()).thenReturn("issue-changes"); - when(qualityGateChange.getKey()).thenReturn("QGateChange"); - when(qualityGateChange.getType()).thenReturn("qgate-changes"); - when(manager.getFromQueue()).thenReturn(notification).thenReturn(null); - - Settings settings = new MapSettings().setProperty("sonar.notifications.delay", 1L); - - service = new NotificationService(settings, manager, - dbClient, - new NotificationDispatcher[]{commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange}); - } - - /** - * Given: - * Simon wants to receive notifications by email on comments for reviews assigned to him or created by him. - *

- * When: - * Freddy adds comment to review created by Simon and assigned to Simon. - *

- * Then: - * Only one notification should be delivered to Simon by Email. - */ - @Test - public void scenario1() { - setUpMocks(); - doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - service.stop(); - - verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); - } - - /** - * Given: - * Evgeny wants to receive notification by GTalk on comments for reviews created by him. - * Simon wants to receive notification by Email on comments for reviews assigned to him. - *

- * When: - * Freddy adds comment to review created by Evgeny and assigned to Simon. - *

- * Then: - * Two notifications should be delivered - one to Simon by Email and another to Evgeny by GTalk. - */ - @Test - public void scenario2() { - setUpMocks(); - doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - doAnswer(addUser(CREATOR_EVGENY, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - verify(gtalkChannel, timeout(2000)).deliver(notification, CREATOR_EVGENY); - service.stop(); - - verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); - verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); - } - - /** - * Given: - * Simon wants to receive notifications by Email and GTLak on comments for reviews assigned to him. - *

- * When: - * Freddy adds comment to review created by Evgeny and assigned to Simon. - *

- * Then: - * Two notifications should be delivered to Simon - one by Email and another by GTalk. - */ - @Test - public void scenario3() { - setUpMocks(); - doAnswer(addUser(ASSIGNEE_SIMON, new NotificationChannel[]{emailChannel, gtalkChannel})) - .when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - verify(gtalkChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - service.stop(); - - verify(emailChannel, never()).deliver(notification, CREATOR_EVGENY); - verify(gtalkChannel, never()).deliver(notification, CREATOR_EVGENY); - } - - /** - * Given: - * Nobody wants to receive notifications. - *

- * When: - * Freddy adds comment to review created by Evgeny and assigned to Simon. - *

- * Then: - * No notifications. - */ - @Test - public void scenario4() { - setUpMocks(); - - service.start(); - service.stop(); - - verify(emailChannel, never()).deliver(any(Notification.class), anyString()); - verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); - } - - // SONAR-4548 - @Test - public void shouldNotStopWhenException() { - setUpMocks(); - when(manager.getFromQueue()).thenThrow(new RuntimeException("Unexpected exception")).thenReturn(notification).thenReturn(null); - doAnswer(addUser(ASSIGNEE_SIMON, emailChannel)).when(commentOnIssueAssignedToMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - doAnswer(addUser(CREATOR_SIMON, emailChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - verify(emailChannel, timeout(2000)).deliver(notification, ASSIGNEE_SIMON); - service.stop(); - - verify(gtalkChannel, never()).deliver(notification, ASSIGNEE_SIMON); - } - - @Test - public void shouldNotAddNullAsUser() { - setUpMocks(); - doAnswer(addUser(null, gtalkChannel)).when(commentOnIssueCreatedByMe).dispatch(same(notification), any(NotificationDispatcher.Context.class)); - - service.start(); - service.stop(); - - verify(emailChannel, never()).deliver(any(Notification.class), anyString()); - verify(gtalkChannel, never()).deliver(any(Notification.class), anyString()); - } - - @Test - public void getDispatchers() { - setUpMocks(); - - assertThat(service.getDispatchers()).containsOnly(commentOnIssueAssignedToMe, commentOnIssueCreatedByMe, qualityGateChange); - } - - @Test - public void getDispatchers_empty() { - Settings settings = new MapSettings().setProperty("sonar.notifications.delay", 1L); - - service = new NotificationService(settings, manager, dbClient); - assertThat(service.getDispatchers()).hasSize(0); - } - - @Test - public void shouldLogEvery10Minutes() { - setUpMocks(); - // Emulate 2 notifications in DB - when(manager.getFromQueue()).thenReturn(notification).thenReturn(notification).thenReturn(null); - when(manager.count()).thenReturn(1L).thenReturn(0L); - service = spy(service); - // Emulate processing of each notification take 10 min to have a log each time - when(service.now()).thenReturn(0L).thenReturn(10 * 60 * 1000 + 1L).thenReturn(20 * 60 * 1000 + 2L); - service.start(); - verify(service, timeout(200)).log(1, 1, 10); - verify(service, timeout(200)).log(2, 0, 20); - service.stop(); - } - - @Test - public void hasProjectSubscribersForType() { - setUpMocks(); - - PropertiesDao dao = mock(PropertiesDao.class); - when(dbClient.propertiesDao()).thenReturn(dao); - - // no subscribers - when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(false); - assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isFalse(); - - // has subscribers on one dispatcher (among the two) - when(dao.hasProjectNotificationSubscribersForDispatchers("PROJECT_UUID", Arrays.asList("CommentOnIssueAssignedToMe", "CommentOnIssueCreatedByMe"))).thenReturn(true); - assertThat(service.hasProjectSubscribersForTypes("PROJECT_UUID", Sets.newHashSet("issue-changes"))).isTrue(); - } - - private static Answer addUser(final String user, final NotificationChannel channel) { - return addUser(user, new NotificationChannel[]{channel}); - } - - private static Answer addUser(final String user, final NotificationChannel[] channels) { - return new Answer() { - public Object answer(InvocationOnMock invocation) { - for (NotificationChannel channel : channels) { - ((NotificationDispatcher.Context) invocation.getArguments()[1]).addUser(user, channel); - } - return null; - } - }; - } -}