}
private static void waitUntilAllNotificationsAreDelivered() throws InterruptedException {
- Thread.sleep(10000);
+ Thread.sleep(10_000L);
}
}
--- /dev/null
+/*
+ * 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();
+ }
+}
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<NotificationDispatcher> 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);
}
/**
* 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
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;
NotificationCenter.class,
DefaultNotificationManager.class,
EmailsWsModule.class,
+ NotificationDaemon.class,
// Tests
TestsWs.class,
--- /dev/null
+/*
+ * 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.
+ * <p/>
+ * When:
+ * Freddy adds comment to review created by Simon and assigned to Simon.
+ * <p/>
+ * 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.
+ * <p/>
+ * When:
+ * Freddy adds comment to review created by Evgeny and assigned to Simon.
+ * <p/>
+ * 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.
+ * <p/>
+ * When:
+ * Freddy adds comment to review created by Evgeny and assigned to Simon.
+ * <p/>
+ * 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.
+ * <p/>
+ * When:
+ * Freddy adds comment to review created by Evgeny and assigned to Simon.
+ * <p/>
+ * 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<Object> addUser(final String user, final NotificationChannel channel) {
+ return addUser(user, new NotificationChannel[]{channel});
+ }
+
+ private static Answer<Object> addUser(final String user, final NotificationChannel[] channels) {
+ return new Answer<Object>() {
+ public Object answer(InvocationOnMock invocation) {
+ for (NotificationChannel channel : channels) {
+ ((NotificationDispatcher.Context) invocation.getArguments()[1]).addUser(user, channel);
+ }
+ return null;
+ }
+ };
+ }
+}
+++ /dev/null
-/*
- * 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.
- * <p/>
- * When:
- * Freddy adds comment to review created by Simon and assigned to Simon.
- * <p/>
- * 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.
- * <p/>
- * When:
- * Freddy adds comment to review created by Evgeny and assigned to Simon.
- * <p/>
- * 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.
- * <p/>
- * When:
- * Freddy adds comment to review created by Evgeny and assigned to Simon.
- * <p/>
- * 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.
- * <p/>
- * When:
- * Freddy adds comment to review created by Evgeny and assigned to Simon.
- * <p/>
- * 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<Object> addUser(final String user, final NotificationChannel channel) {
- return addUser(user, new NotificationChannel[]{channel});
- }
-
- private static Answer<Object> addUser(final String user, final NotificationChannel[] channels) {
- return new Answer<Object>() {
- public Object answer(InvocationOnMock invocation) {
- for (NotificationChannel channel : channels) {
- ((NotificationDispatcher.Context) invocation.getArguments()[1]).addUser(user, channel);
- }
- return null;
- }
- };
- }
-}