SONAR-2746 Email new alerts on favorite projects

This commit is contained in:
Fabrice Bellingard 2013-01-24 16:07:51 +01:00
parent aac947247b
commit 30d668f520
8 changed files with 463 additions and 12 deletions

View File

@ -19,10 +19,18 @@
*/
package org.sonar.plugins.core.sensors;
import org.sonar.api.batch.*;
import org.sonar.api.batch.Decorator;
import org.sonar.api.batch.DecoratorContext;
import org.sonar.api.batch.DependsUpon;
import org.sonar.api.batch.Event;
import org.sonar.api.batch.TimeMachine;
import org.sonar.api.batch.TimeMachineQuery;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Measure;
import org.sonar.api.measures.Metric;
import org.sonar.api.measures.Metric.Level;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.resources.Project;
import org.sonar.api.resources.Resource;
@ -34,17 +42,18 @@ public class GenerateAlertEvents implements Decorator {
private final RulesProfile profile;
private final TimeMachine timeMachine;
private NotificationManager notificationManager;
public GenerateAlertEvents(RulesProfile profile, TimeMachine timeMachine) {
public GenerateAlertEvents(RulesProfile profile, TimeMachine timeMachine, NotificationManager notificationManager) {
this.profile = profile;
this.timeMachine = timeMachine;
this.notificationManager = notificationManager;
}
public boolean shouldExecuteOnProject(Project project) {
return profile != null && profile.getAlerts() != null && profile.getAlerts().size() > 0;
}
@DependsUpon
public Metric dependsUponAlertStatus() {
return CoreMetrics.ALERT_STATUS;
@ -63,15 +72,41 @@ public class GenerateAlertEvents implements Decorator {
List<Measure> measures = timeMachine.getMeasures(query);
Measure pastStatus = (measures != null && measures.size() == 1 ? measures.get(0) : null);
if (pastStatus != null && pastStatus.getDataAsLevel() != currentStatus.getDataAsLevel()) {
createEvent(context, getName(pastStatus, currentStatus), currentStatus.getAlertText());
String alertText = currentStatus.getAlertText();
Level alertLevel = currentStatus.getDataAsLevel();
String alertName = null;
boolean isNewAlert = true;
if (pastStatus != null && pastStatus.getDataAsLevel() != alertLevel) {
// The alert status has changed
alertName = getName(pastStatus, currentStatus);
if (pastStatus.getDataAsLevel() != Metric.Level.OK) {
// There was already a Orange/Red alert, so this is no new alert: it has just changed
isNewAlert = false;
}
createEvent(context, alertName, alertText);
notifyUsers(resource, alertName, alertText, alertLevel, isNewAlert);
} else if (pastStatus == null && currentStatus.getDataAsLevel() != Metric.Level.OK) {
createEvent(context, getName(currentStatus), currentStatus.getAlertText());
} else if (pastStatus == null && alertLevel != Metric.Level.OK) {
// There were no defined alerts before, so this one is a new one
alertName = getName(currentStatus);
createEvent(context, alertName, alertText);
notifyUsers(resource, alertName, alertText, alertLevel, isNewAlert);
}
}
protected void notifyUsers(Resource<?> resource, String alertName, String alertText, Level alertLevel, boolean isNewAlert) {
Notification notification = new Notification("alerts")
.setFieldValue("projectName", resource.getLongName())
.setFieldValue("projectKey", resource.getKey())
.setFieldValue("projectId", String.valueOf(resource.getId()))
.setFieldValue("alertName", alertName)
.setFieldValue("alertText", alertText)
.setFieldValue("alertLevel", alertLevel.toString())
.setFieldValue("isNewAlert", Boolean.toString(isNewAlert));
notificationManager.scheduleForSending(notification);
}
private boolean shouldDecorateResource(Resource resource) {
return ResourceUtils.isRootProject(resource);
}

View File

@ -1495,6 +1495,7 @@ server_id_configuration.generation_error=Organisation and/or IP address are not
notification.channel.EmailNotificationChannel=Email
notification.dispatcher.ChangesInReviewAssignedToMeOrCreatedByMe=Changes in review assigned to me or created by me
notification.dispatcher.NewViolationsOnMyFavouriteProject=New violations on my favourite projects introduced during the first differential view period
notification.dispatcher.AlertsOnMyFavouriteProject=Alerts on my favourite projects
#------------------------------------------------------------------------------

View File

@ -28,21 +28,27 @@ import org.sonar.api.batch.TimeMachineQuery;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Measure;
import org.sonar.api.measures.Metric;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationManager;
import org.sonar.api.profiles.Alert;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.resources.File;
import org.sonar.api.resources.Project;
import org.sonar.api.test.ProjectTestBuilder;
import java.util.Arrays;
import java.util.Date;
import static org.fest.assertions.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -51,6 +57,7 @@ public class GenerateAlertEventsTest {
private DecoratorContext context;
private RulesProfile profile;
private TimeMachine timeMachine;
private NotificationManager notificationManager;
private Project project;
@Before
@ -58,12 +65,18 @@ public class GenerateAlertEventsTest {
context = mock(DecoratorContext.class);
timeMachine = mock(TimeMachine.class);
profile = mock(RulesProfile.class);
decorator = new GenerateAlertEvents(profile, timeMachine);
notificationManager = mock(NotificationManager.class);
decorator = new GenerateAlertEvents(profile, timeMachine, notificationManager);
project = new ProjectTestBuilder().build();
}
@Test
public void doNotDecorateIfNoThresholds() {
public void shouldDependUponAlertStatus() {
assertThat(decorator.dependsUponAlertStatus()).isEqualTo(CoreMetrics.ALERT_STATUS);
}
@Test
public void shouldNotDecorateIfNoThresholds() {
assertThat(decorator.shouldExecuteOnProject(project), is(false));
}
@ -73,18 +86,30 @@ public class GenerateAlertEventsTest {
assertThat(decorator.shouldExecuteOnProject(project), is(true));
}
@Test
public void shouldNotDecorateIfNotRootProject() {
decorator.decorate(new File("Foo"), context);
verify(context, never()).createEvent(anyString(), anyString(), anyString(), (Date) isNull());
}
@Test
public void shouldCreateEventWhenNewErrorAlert() {
when(context.getMeasure(CoreMetrics.ALERT_STATUS)).thenReturn(newAlertStatus(Metric.Level.ERROR, "desc"));
decorator.decorate(project, context);
verify(context).createEvent(Metric.Level.ERROR.getColorName(), "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Red", "desc", "ERROR", "true");
}
@Test
public void shouldCreateEventWhenNewWarnAlert() {
when(context.getMeasure(CoreMetrics.ALERT_STATUS)).thenReturn(newAlertStatus(Metric.Level.WARN, "desc"));
decorator.decorate(project, context);
verify(context).createEvent(Metric.Level.WARN.getColorName(), "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Orange", "desc", "WARN", "true");
}
@Test
@ -95,6 +120,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);
verify(context).createEvent("Red (was Orange)", "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Red (was Orange)", "desc", "ERROR", "false");
}
@Test
@ -105,6 +131,18 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);
verify(context).createEvent("Green (was Red)", null, Event.CATEGORY_ALERT, null);
verifyNotificationSent("Green (was Red)", null, "OK", "false");
}
@Test
public void shouldCreateEventWhenOkToError() {
when(timeMachine.getMeasures(any(TimeMachineQuery.class))).thenReturn(Arrays.asList(newAlertStatus(Metric.Level.OK, null)));
when(context.getMeasure(CoreMetrics.ALERT_STATUS)).thenReturn(newAlertStatus(Metric.Level.ERROR, "desc"));
decorator.decorate(project, context);
verify(context).createEvent("Red (was Green)", "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Red (was Green)", "desc", "ERROR", "true");
}
@Test
@ -115,6 +153,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);
verify(context).createEvent("Orange (was Red)", "desc", Event.CATEGORY_ALERT, null);
verifyNotificationSent("Orange (was Red)", "desc", "WARN", "false");
}
@Test
@ -122,6 +161,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);
verify(context, never()).createEvent(anyString(), anyString(), anyString(), (Date) isNull());
verify(notificationManager, never()).scheduleForSending(any(Notification.class));
}
@Test
@ -132,6 +172,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);
verify(context, never()).createEvent(anyString(), anyString(), anyString(), (Date) isNull());
verify(notificationManager, never()).scheduleForSending(any(Notification.class));
}
@Test
@ -142,6 +183,7 @@ public class GenerateAlertEventsTest {
decorator.decorate(project, context);
verify(context, never()).createEvent(anyString(), anyString(), anyString(), (Date) isNull());
verify(notificationManager, never()).scheduleForSending(any(Notification.class));
}
private Measure newAlertStatus(Metric.Level level, String label) {
@ -150,4 +192,16 @@ public class GenerateAlertEventsTest {
measure.setAlertText(label);
return measure;
}
private void verifyNotificationSent(String alertName, String alertText, String alertLevel, String isNewAlert) {
Notification notification = new Notification("alerts")
.setFieldValue("projectName", project.getLongName())
.setFieldValue("projectKey", project.getKey())
.setFieldValue("projectId", String.valueOf(project.getId()))
.setFieldValue("alertName", alertName)
.setFieldValue("alertText", alertText)
.setFieldValue("alertLevel", alertLevel)
.setFieldValue("isNewAlert", isNewAlert);
verify(notificationManager, times(1)).scheduleForSending(eq(notification));
}
}

View File

@ -22,6 +22,8 @@ package org.sonar.plugins.emailnotifications;
import com.google.common.collect.ImmutableList;
import org.sonar.api.ServerExtension;
import org.sonar.api.SonarPlugin;
import org.sonar.plugins.emailnotifications.alerts.AlertsEmailTemplate;
import org.sonar.plugins.emailnotifications.alerts.AlertsOnMyFavouriteProject;
import org.sonar.plugins.emailnotifications.newviolations.NewViolationsEmailTemplate;
import org.sonar.plugins.emailnotifications.newviolations.NewViolationsOnMyFavouriteProject;
import org.sonar.plugins.emailnotifications.reviews.ChangesInReviewAssignedToMeOrCreatedByMe;
@ -32,10 +34,16 @@ import java.util.List;
public class EmailNotificationsPlugin extends SonarPlugin {
public List<Class<? extends ServerExtension>> getExtensions() {
return ImmutableList.of(
ChangesInReviewAssignedToMeOrCreatedByMe.class,
EmailNotificationChannel.class,
NewViolationsEmailTemplate.class,
// Notify incoming violations on my favourite projects
NewViolationsOnMyFavouriteProject.class,
ReviewEmailTemplate.class);
NewViolationsEmailTemplate.class,
// Notify reviews changes
ChangesInReviewAssignedToMeOrCreatedByMe.class,
ReviewEmailTemplate.class,
// Notify alerts on my favourite projects
AlertsOnMyFavouriteProject.class,
AlertsEmailTemplate.class
);
}
}

View File

@ -0,0 +1,107 @@
/*
* Sonar, open source software quality management tool.
* Copyright (C) 2008-2012 SonarSource
* mailto:contact AT sonarsource DOT com
*
* Sonar 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.
*
* Sonar 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 Sonar; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonar.plugins.emailnotifications.alerts;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.measures.Metric;
import org.sonar.api.notifications.Notification;
import org.sonar.plugins.emailnotifications.api.EmailMessage;
import org.sonar.plugins.emailnotifications.api.EmailTemplate;
/**
* Creates email message for notification "alerts".
*
* @since 3.5
*/
public class AlertsEmailTemplate extends EmailTemplate {
private EmailSettings configuration;
public AlertsEmailTemplate(EmailSettings configuration) {
this.configuration = configuration;
}
@Override
public EmailMessage format(Notification notification) {
if (!"alerts".equals(notification.getType())) {
return null;
}
// Retrieve useful values
String projectId = notification.getFieldValue("projectId");
String projectKey = notification.getFieldValue("projectKey");
String projectName = notification.getFieldValue("projectName");
String alertName = notification.getFieldValue("alertName");
String alertText = notification.getFieldValue("alertText");
String alertLevel = notification.getFieldValue("alertLevel");
boolean isNewAlert = Boolean.valueOf(notification.getFieldValue("isNewAlert"));
// Generate text
String subject = generateSubject(projectName, alertLevel, isNewAlert);
String messageBody = generateMessageBody(projectName, projectKey, alertName, alertText, isNewAlert);
// And finally return the email that will be sent
return new EmailMessage()
.setMessageId("alerts/" + projectId)
.setSubject(subject)
.setMessage(messageBody);
}
private String generateSubject(String projectName, String alertLevel, boolean isNewAlert) {
StringBuilder subjectBuilder = new StringBuilder();
if (Metric.Level.OK.toString().equals(alertLevel)) {
subjectBuilder.append("\"").append(projectName).append("\" is back to green");
} else if (isNewAlert) {
subjectBuilder.append("New alert on \"").append(projectName).append("\"");
} else {
subjectBuilder.append("Alert level changed on \"").append(projectName).append("\"");
}
return subjectBuilder.toString();
}
private String generateMessageBody(String projectName, String projectKey, String alertName, String alertText, boolean isNewAlert) {
StringBuilder messageBody = new StringBuilder();
messageBody.append("Project: ").append(projectName).append("\n");
messageBody.append("Alert level: ").append(alertName).append("\n\n");
String[] alerts = StringUtils.split(alertText, ",");
if (alerts.length > 0) {
if (isNewAlert) {
messageBody.append("New alert");
} else {
messageBody.append("Alert");
}
if (alerts.length == 1) {
messageBody.append(": ").append(alerts[0].trim()).append("\n");
} else {
messageBody.append("s:\n");
for (String alert : alerts) {
messageBody.append(" - ").append(alert.trim()).append("\n");
}
}
}
messageBody.append("\n").append("See it in Sonar: ").append(configuration.getServerBaseURL()).append("/dashboard/index/").append(projectKey);
return messageBody.toString();
}
}

View File

@ -0,0 +1,53 @@
/*
* Sonar, open source software quality management tool.
* Copyright (C) 2008-2012 SonarSource
* mailto:contact AT sonarsource DOT com
*
* Sonar 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.
*
* Sonar 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 Sonar; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonar.plugins.emailnotifications.alerts;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.core.properties.PropertiesDao;
import java.util.List;
/**
* This dispatcher means: "notify me when alerts are raised on projects that I flagged as favourite".
*
* @since 3.5
*/
public class AlertsOnMyFavouriteProject extends NotificationDispatcher {
private PropertiesDao propertiesDao;
public AlertsOnMyFavouriteProject(PropertiesDao propertiesDao) {
this.propertiesDao = propertiesDao;
}
@Override
public void dispatch(Notification notification, Context context) {
if (StringUtils.equals(notification.getType(), "alerts")) {
Long projectId = Long.parseLong(notification.getFieldValue("projectId"));
List<String> userLogins = propertiesDao.findUserIdsForFavouriteResource(projectId);
for (String userLogin : userLogins) {
context.addUser(userLogin);
}
}
}
}

View File

@ -0,0 +1,131 @@
/*
* Sonar, open source software quality management tool.
* Copyright (C) 2008-2012 SonarSource
* mailto:contact AT sonarsource DOT com
*
* Sonar 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.
*
* Sonar 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 Sonar; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonar.plugins.emailnotifications.alerts;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.config.EmailSettings;
import org.sonar.api.notifications.Notification;
import org.sonar.plugins.emailnotifications.api.EmailMessage;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class AlertsEmailTemplateTest {
private AlertsEmailTemplate template;
@Before
public void setUp() {
EmailSettings configuration = mock(EmailSettings.class);
when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org");
template = new AlertsEmailTemplate(configuration);
}
@Test
public void shouldNotFormatIfNotCorrectNotification() {
Notification notification = new Notification("other-notif");
EmailMessage message = template.format(notification);
assertThat(message, nullValue());
}
@Test
public void shouldFormatAlertWithSeveralMessages() {
Notification notification = createNotification("Orange (was Red)", "violations > 4, coverage < 75%", "WARN", "false");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId(), is("alerts/45"));
assertThat(message.getSubject(), is("Alert level changed on \"Foo\""));
assertThat(message.getMessage(), is("" +
"Project: Foo\n" +
"Alert level: Orange (was Red)\n" +
"\n" +
"Alerts:\n" +
" - violations > 4\n" +
" - coverage < 75%\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo"));
}
@Test
public void shouldFormatNewAlertWithSeveralMessages() {
Notification notification = createNotification("Orange (was Red)", "violations > 4, coverage < 75%", "WARN", "true");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId(), is("alerts/45"));
assertThat(message.getSubject(), is("New alert on \"Foo\""));
assertThat(message.getMessage(), is("" +
"Project: Foo\n" +
"Alert level: Orange (was Red)\n" +
"\n" +
"New alerts:\n" +
" - violations > 4\n" +
" - coverage < 75%\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo"));
}
@Test
public void shouldFormatNewAlertWithOneMessage() {
Notification notification = createNotification("Orange (was Red)", "violations > 4", "WARN", "true");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId(), is("alerts/45"));
assertThat(message.getSubject(), is("New alert on \"Foo\""));
assertThat(message.getMessage(), is("" +
"Project: Foo\n" +
"Alert level: Orange (was Red)\n" +
"\n" +
"New alert: violations > 4\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo"));
}
@Test
public void shouldFormatBackToGreenMessage() {
Notification notification = createNotification("Green (was Red)", "", "OK", "false");
EmailMessage message = template.format(notification);
assertThat(message.getMessageId(), is("alerts/45"));
assertThat(message.getSubject(), is("\"Foo\" is back to green"));
assertThat(message.getMessage(), is("" +
"Project: Foo\n" +
"Alert level: Green (was Red)\n" +
"\n" +
"\n" +
"See it in Sonar: http://nemo.sonarsource.org/dashboard/index/org.sonar.foo:foo"));
}
private Notification createNotification(String alertName, String alertText, String alertLevel, String isNewAlert) {
Notification notification = new Notification("alerts")
.setFieldValue("projectName", "Foo")
.setFieldValue("projectKey", "org.sonar.foo:foo")
.setFieldValue("projectId", "45")
.setFieldValue("alertName", alertName)
.setFieldValue("alertText", alertText)
.setFieldValue("alertLevel", alertLevel)
.setFieldValue("isNewAlert", isNewAlert);
return notification;
}
}

View File

@ -0,0 +1,62 @@
/*
* Sonar, open source software quality management tool.
* Copyright (C) 2008-2012 SonarSource
* mailto:contact AT sonarsource DOT com
*
* Sonar 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.
*
* Sonar 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 Sonar; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonar.plugins.emailnotifications.alerts;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationDispatcher;
import org.sonar.core.properties.PropertiesDao;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class AlertsOnMyFavouriteProjectTest {
@Test
public void shouldNotDispatchIfNotNewViolationsNotification() throws Exception {
NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class);
AlertsOnMyFavouriteProject dispatcher = new AlertsOnMyFavouriteProject(null);
Notification notification = new Notification("other-notif");
dispatcher.dispatch(notification, context);
verify(context, never()).addUser(any(String.class));
}
@Test
public void shouldDispatchToUsersWhoHaveFlaggedProjectAsFavourite() {
NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class);
PropertiesDao propertiesDao = mock(PropertiesDao.class);
when(propertiesDao.findUserIdsForFavouriteResource(34L)).thenReturn(Lists.newArrayList("user1", "user2"));
AlertsOnMyFavouriteProject dispatcher = new AlertsOnMyFavouriteProject(propertiesDao);
Notification notification = new Notification("alerts").setFieldValue("projectId", "34");
dispatcher.dispatch(notification, context);
verify(context).addUser("user1");
verify(context).addUser("user2");
verifyNoMoreInteractions(context);
}
}