]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11753 NotificationDaemon supports email specific algo
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 25 Mar 2019 16:11:44 +0000 (17:11 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 23 Apr 2019 08:37:53 +0000 (10:37 +0200)
server/sonar-server/src/main/java/org/sonar/server/notification/NotificationDaemon.java
server/sonar-server/src/test/java/org/sonar/server/notification/NotificationDaemonTest.java
server/sonar-server/src/test/java/org/sonar/server/notification/NotificationMediumTest.java [new file with mode: 0644]

index 946db5e86a82d7edd24063302627380c6841df07..acf90de95865871779805c78d0ff122ff2122ac5 100644 (file)
@@ -33,6 +33,8 @@ import org.sonar.api.server.ServerSide;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 
+import static java.util.Collections.singleton;
+
 @Properties({
   @Property(
     key = NotificationDaemon.PROPERTY_DELAY,
@@ -106,8 +108,9 @@ public class NotificationDaemon implements Startable {
 
     Notification notifToSend = manager.getFromQueue();
     while (notifToSend != null) {
-      service.deliver(notifToSend);
-      notifSentCount++;
+      notifSentCount += service.deliverEmails(singleton(notifToSend));
+      // compatibility with old API
+      notifSentCount += service.deliver(notifToSend);
       if (stopping) {
         break;
       }
index 8c43891542e236c0537b5502f432f9f433a26ff1..8a24e6b092c50b5837ce0e9605da642abfc780fd 100644 (file)
  */
 package org.sonar.server.notification;
 
-import com.google.common.collect.Sets;
-import java.util.Arrays;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.mockito.verification.Timeout;
 import org.sonar.api.config.PropertyDefinitions;
-import org.sonar.api.config.Settings;
 import org.sonar.api.config.internal.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.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.same;
-import static org.mockito.Mockito.doAnswer;
+import static java.util.Collections.singleton;
+import static org.mockito.ArgumentMatchers.anyCollection;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-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);
+  private NotificationService notificationService = mock(NotificationService.class);
+  private NotificationDaemon underTest;
+  private InOrder inOrder;
 
+  @Before
+  public void setUp() throws Exception {
     MapSettings settings = new MapSettings(new PropertyDefinitions(NotificationDaemon.class)).setProperty("sonar.notifications.delay", 1L);
 
-    underTest = new NotificationDaemon(settings.asConfig(), 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);
+    underTest = new NotificationDaemon(settings.asConfig(), manager, notificationService);
+    inOrder = Mockito.inOrder(notificationService);
   }
 
-  /**
-   * 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);
+  @After
+  public void tearDown() {
     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();
+  public void no_effect_when_no_notification() {
+    when(manager.getFromQueue()).thenReturn(null);
 
     underTest.start();
+    inOrder.verify(notificationService, new Timeout(2000, Mockito.times(0))).deliverEmails(anyCollection());
+    inOrder.verifyNoMoreInteractions();
     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));
+  public void calls_both_api_and_deprecated_API() {
+    Notification notification = mock(Notification.class);
+    when(manager.getFromQueue()).thenReturn(notification).thenReturn(null);
 
     underTest.start();
+    inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification));
+    inOrder.verify(notificationService).deliver(notification);
+    inOrder.verifyNoMoreInteractions();
     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);
+  public void notifications_are_processed_one_by_one_even_with_new_API() {
+    Notification notification1 = mock(Notification.class);
+    Notification notification2 = mock(Notification.class);
+    Notification notification3 = mock(Notification.class);
+    Notification notification4 = mock(Notification.class);
+    when(manager.getFromQueue())
+      .thenReturn(notification1)
+      .thenReturn(notification2)
+      .thenReturn(notification3)
+      .thenReturn(notification4)
+      .thenReturn(null);
 
-    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);
+    inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification1));
+    inOrder.verify(notificationService).deliver(notification1);
+    inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification2));
+    inOrder.verify(notificationService).deliver(notification2);
+    inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification3));
+    inOrder.verify(notificationService).deliver(notification3);
+    inOrder.verify(notificationService, timeout(2000)).deliverEmails(singleton(notification4));
+    inOrder.verify(notificationService).deliver(notification4);
+    inOrder.verifyNoMoreInteractions();
     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;
-      }
-    };
   }
 }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/notification/NotificationMediumTest.java
new file mode 100644 (file)
index 0000000..5df98ad
--- /dev/null
@@ -0,0 +1,261 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info 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.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.config.internal.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.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+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 NotificationMediumTest {
+  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);
+
+    MapSettings settings = new MapSettings(new PropertyDefinitions(NotificationDaemon.class)).setProperty("sonar.notifications.delay", 1L);
+
+    underTest = new NotificationDaemon(settings.asConfig(), 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;
+      }
+    };
+  }
+}