Browse Source

SONAR-11753 move "My new issues" notification to email specific algo

tags/7.8
Sébastien Lesaint 5 years ago
parent
commit
d39e02878c
27 changed files with 1564 additions and 241 deletions
  1. 16
    7
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStep.java
  2. 22
    6
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java
  3. 3
    3
      server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
  4. 76
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/property/EmailSubscriberDto.java
  5. 5
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesDao.java
  6. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesMapper.java
  7. 45
    7
      server/sonar-db-dao/src/main/resources/org/sonar/db/property/PropertiesMapper.xml
  8. 139
    7
      server/sonar-db-dao/src/test/java/org/sonar/db/property/PropertiesDaoTest.java
  9. 11
    0
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotification.java
  10. 0
    69
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcher.java
  11. 108
    0
      server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationHandler.java
  12. 54
    1
      server/sonar-server-common/src/main/java/org/sonar/server/notification/DefaultNotificationManager.java
  13. 33
    0
      server/sonar-server-common/src/main/java/org/sonar/server/notification/NotificationHandler.java
  14. 47
    2
      server/sonar-server-common/src/main/java/org/sonar/server/notification/NotificationManager.java
  15. 55
    3
      server/sonar-server-common/src/main/java/org/sonar/server/notification/NotificationService.java
  16. 75
    4
      server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java
  17. 0
    72
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcherTest.java
  18. 355
    0
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationHandlerTest.java
  19. 21
    2
      server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationTest.java
  20. 168
    45
      server/sonar-server-common/src/test/java/org/sonar/server/notification/DefaultNotificationManagerTest.java
  21. 92
    0
      server/sonar-server-common/src/test/java/org/sonar/server/notification/EmailRecipientTest.java
  22. 202
    0
      server/sonar-server-common/src/test/java/org/sonar/server/notification/NotificationServiceTest.java
  23. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java
  24. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java
  25. 3
    3
      server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  26. 22
    0
      server/sonar-server/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java
  27. 6
    6
      server/sonar-server/src/test/java/org/sonar/server/notification/ws/DispatchersImplTest.java

+ 16
- 7
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStep.java View File

@@ -46,6 +46,7 @@ import org.sonar.ce.task.projectanalysis.issue.RuleRepository;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.util.CloseableIterator;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
@@ -99,9 +100,10 @@ public class SendIssueNotificationsStep implements ComputationStep {
public void execute(ComputationStep.Context context) {
Component project = treeRootHolder.getRoot();
NotificationStatistics notificationStatistics = new NotificationStatistics();
if (service.hasProjectSubscribersForTypes(project.getUuid(), NOTIF_TYPES)) {
// FIXME do we still need this fail fast?
// if (service.hasProjectSubscribersForTypes(project.getUuid(), NOTIF_TYPES)) {
doExecute(notificationStatistics, project);
}
// }
notificationStatistics.dumpTo(context);
}

@@ -175,10 +177,10 @@ public class SendIssueNotificationsStep implements ComputationStep {

private void sendMyNewIssuesNotification(NewIssuesStatistics statistics, Component project, long analysisDate, NotificationStatistics notificationStatistics) {
Map<String, UserDto> userDtoByUuid = loadUserDtoByUuid(statistics);
statistics.getAssigneesStatistics().entrySet()
Set<MyNewIssuesNotification> myNewIssuesNotifications = statistics.getAssigneesStatistics().entrySet()
.stream()
.filter(e -> e.getValue().hasIssuesOnLeak())
.forEach(e -> {
.map(e -> {
String assigneeUuid = e.getKey();
NewIssuesStatistics.Stats assigneeStatistics = e.getValue();
MyNewIssuesNotification myNewIssuesNotification = newIssuesNotificationFactory
@@ -191,9 +193,16 @@ public class SendIssueNotificationsStep implements ComputationStep {
.setStatistics(project.getName(), assigneeStatistics)
.setDebt(Duration.create(assigneeStatistics.effort().getOnLeak()));

notificationStatistics.myNewIssuesDeliveries += service.deliver(myNewIssuesNotification);
notificationStatistics.myNewIssues++;
});
return myNewIssuesNotification;
})
.collect(MoreCollectors.toSet(statistics.getAssigneesStatistics().size()));

notificationStatistics.myNewIssuesDeliveries += service.deliverEmails(myNewIssuesNotifications);
notificationStatistics.myNewIssues += myNewIssuesNotifications.size();

// compatibility with old API
myNewIssuesNotifications
.forEach(e -> notificationStatistics.myNewIssuesDeliveries += service.deliver(e));
}

private Map<String, UserDto> loadUserDtoByUuid(NewIssuesStatistics statistics) {

+ 22
- 6
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/SendIssueNotificationsStepTest.java View File

@@ -19,6 +19,7 @@
*/
package org.sonar.ce.task.projectanalysis.step;

import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
@@ -32,6 +33,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentCaptor;
import org.sonar.api.notifications.Notification;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.Duration;
import org.sonar.api.utils.System2;
@@ -66,6 +68,7 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.apache.commons.lang.math.RandomUtils.nextInt;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentCaptor.forClass;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
@@ -141,7 +144,8 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
TestComputationStepContext context = new TestComputationStepContext();
underTest.execute(context);

verify(notificationService, never()).deliver(any());
verify(notificationService, never()).deliver(any(Notification.class));
verify(notificationService, never()).deliverEmails(anyCollection());
verifyStatistics(context, 0, 0, 0);
}

@@ -209,7 +213,8 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
TestComputationStepContext context = new TestComputationStepContext();
underTest.execute(context);

verify(notificationService, never()).deliver(any());
verify(notificationService, never()).deliver(any(Notification.class));
verify(notificationService, never()).deliverEmails(anyCollection());
verifyStatistics(context, 0, 0, 0);
}

@@ -263,7 +268,8 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
TestComputationStepContext context = new TestComputationStepContext();
underTest.execute(context);

verify(notificationService, never()).deliver(any());
verify(notificationService, never()).deliver(any(Notification.class));
verify(notificationService, never()).deliverEmails(anyCollection());
verifyStatistics(context, 0, 0, 0);
}

@@ -281,6 +287,8 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
underTest.execute(context);

verify(notificationService).deliver(newIssuesNotificationMock);
verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock));
// old API compatibility call
verify(notificationService).deliver(myNewIssuesNotificationMock);
verify(myNewIssuesNotificationMock).setAssignee(any(UserDto.class));
verify(myNewIssuesNotificationMock).setProject(PROJECT.getKey(), PROJECT.getName(), null, null);
@@ -327,13 +335,16 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
new SendIssueNotificationsStep(issueCache, ruleRepository, treeRootHolder, notificationService, analysisMetadataHolder, newIssuesNotificationFactory, db.getDbClient())
.execute(context);

verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock1, myNewIssuesNotificationMock2));
// old API compatibility
verify(notificationService).deliver(myNewIssuesNotificationMock1);
verify(notificationService).deliver(myNewIssuesNotificationMock2);

Map<String, MyNewIssuesNotification> myNewIssuesNotificationMocksByUsersName = new HashMap<>();
ArgumentCaptor<UserDto> userCaptor1 = forClass(UserDto.class);
verify(myNewIssuesNotificationMock1).setAssignee(userCaptor1.capture());
myNewIssuesNotificationMocksByUsersName.put(userCaptor1.getValue().getLogin(), myNewIssuesNotificationMock1);

verify(notificationService).deliver(myNewIssuesNotificationMock2);
ArgumentCaptor<UserDto> userCaptor2 = forClass(UserDto.class);
verify(myNewIssuesNotificationMock2).setAssignee(userCaptor2.capture());
myNewIssuesNotificationMocksByUsersName.put(userCaptor2.getValue().getLogin(), myNewIssuesNotificationMock2);
@@ -378,7 +389,10 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
underTest.execute(context);

verify(notificationService).deliver(newIssuesNotificationMock);
verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock));
// old API compatibility
verify(notificationService).deliver(myNewIssuesNotificationMock);

verify(myNewIssuesNotificationMock).setAssignee(any(UserDto.class));
ArgumentCaptor<NewIssuesStatistics.Stats> statsCaptor = forClass(NewIssuesStatistics.Stats.class);
verify(myNewIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), statsCaptor.capture());
@@ -405,7 +419,8 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
TestComputationStepContext context = new TestComputationStepContext();
underTest.execute(context);

verify(notificationService, never()).deliver(any());
verify(notificationService, never()).deliver(any(Notification.class));
verify(notificationService, never()).deliverEmails(anyCollection());
verifyStatistics(context, 0, 0, 0);
}

@@ -425,7 +440,8 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
TestComputationStepContext context = new TestComputationStepContext();
underTest.execute(context);

verify(notificationService, never()).deliver(any());
verify(notificationService, never()).deliver(any(Notification.class));
verify(notificationService, never()).deliverEmails(anyCollection());
verifyStatistics(context, 0, 0, 0);
}


+ 3
- 3
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java View File

@@ -103,7 +103,7 @@ import org.sonar.server.issue.notification.ChangesOnMyIssueNotificationDispatche
import org.sonar.server.issue.notification.DoNotFixNotificationDispatcher;
import org.sonar.server.issue.notification.IssueChangesEmailTemplate;
import org.sonar.server.issue.notification.MyNewIssuesEmailTemplate;
import org.sonar.server.issue.notification.MyNewIssuesNotificationDispatcher;
import org.sonar.server.issue.notification.MyNewIssuesNotificationHandler;
import org.sonar.server.issue.notification.NewIssuesEmailTemplate;
import org.sonar.server.issue.notification.NewIssuesNotificationDispatcher;
import org.sonar.server.issue.notification.NewIssuesNotificationFactory;
@@ -408,8 +408,8 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
ChangesOnMyIssueNotificationDispatcher.newMetadata(),
NewIssuesNotificationDispatcher.class,
NewIssuesNotificationDispatcher.newMetadata(),
MyNewIssuesNotificationDispatcher.class,
MyNewIssuesNotificationDispatcher.newMetadata(),
MyNewIssuesNotificationHandler.class,
MyNewIssuesNotificationHandler.newMetadata(),
DoNotFixNotificationDispatcher.class,
DoNotFixNotificationDispatcher.newMetadata(),
NewIssuesNotificationFactory.class, // used by SendIssueNotificationsStep

+ 76
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/property/EmailSubscriberDto.java View File

@@ -0,0 +1,76 @@
/*
* 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.db.property;

import java.util.Objects;
import javax.annotation.concurrent.Immutable;

@Immutable
public final class EmailSubscriberDto {
private final String login;
private final boolean global;
private final String email;

public EmailSubscriberDto(String login, boolean global, String email) {
this.login = login;
this.global = global;
this.email = email;
}

public String getLogin() {
return login;
}

public boolean isGlobal() {
return global;
}

public String getEmail() {
return email;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EmailSubscriberDto that = (EmailSubscriberDto) o;
return global == that.global &&
Objects.equals(login, that.login) &&
Objects.equals(email, that.email);
}

@Override
public int hashCode() {
return Objects.hash(login, global, email);
}

@Override
public String toString() {
return "EmailSubscriberDto{" +
"login='" + login + '\'' +
", global=" + global +
", email='" + email + '\'' +
'}';
}
}

+ 5
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesDao.java View File

@@ -69,6 +69,11 @@ public class PropertiesDao implements Dao {
}
}

public Set<EmailSubscriberDto> findEmailSubscribersForNotification(DbSession dbSession, String notificationDispatcherKey, String notificationChannelKey,
@Nullable String projectKey) {
return getMapper(dbSession).findEmailRecipientsForNotification(NOTIFICATION_PREFIX + notificationDispatcherKey + "." + notificationChannelKey, projectKey);
}

public boolean hasProjectNotificationSubscribersForDispatchers(String projectUuid, Collection<String> dispatcherKeys) {
try (DbSession session = mybatis.openSession(false);
Connection connection = session.getConnection();

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesMapper.java View File

@@ -28,6 +28,8 @@ public interface PropertiesMapper {

Set<Subscriber> findUsersForNotification(@Param("notifKey") String notificationKey, @Nullable @Param("projectKey") String projectKey);

Set<EmailSubscriberDto> findEmailRecipientsForNotification(@Param("notifKey") String notificationKey, @Nullable @Param("projectKey") String projectKey);

List<PropertyDto> selectGlobalProperties();

List<PropertyDto> selectProjectProperties(String resourceKey);

+ 45
- 7
server/sonar-db-dao/src/main/resources/org/sonar/db/property/PropertiesMapper.xml View File

@@ -15,19 +15,57 @@
AND p.text_value = 'true'
AND p.resource_id IS NULL

UNION
<if test="projectKey != null">
UNION

SELECT
u.login as "login",
${_false} as "global"
FROM
users u
INNER JOIN projects c on c.kee = #{projectKey,jdbcType=VARCHAR}
INNER JOIN properties p ON p.user_id = u.id
WHERE
p.prop_key = #{notifKey,jdbcType=VARCHAR}
AND p.text_value = 'true'
AND p.resource_id = c.id
</if>
</select>

<select id="findEmailRecipientsForNotification" parameterType="map" resultType="org.sonar.db.property.EmailSubscriberDto">
SELECT
u.login as "login",
${_false} as "global"
${_true} as "global",
u.email as "email"
FROM
users u
INNER JOIN projects c on c.kee = #{projectKey,jdbcType=VARCHAR}
INNER JOIN properties p ON p.user_id = u.id
INNER JOIN properties p ON
p.user_id = u.id
and p.prop_key = #{notifKey,jdbcType=VARCHAR}
and p.text_value = 'true'
and p.resource_id IS NULL
WHERE
p.prop_key = #{notifKey,jdbcType=VARCHAR}
AND p.text_value = 'true'
AND p.resource_id = c.id
u.email is not null

<if test="projectKey != null">
UNION

SELECT
u.login as "login",
${_false} as "global",
u.email as "email"
FROM
users u
INNER JOIN projects c on
c.kee = #{projectKey,jdbcType=VARCHAR}
INNER JOIN properties p ON
p.user_id = u.id
and p.prop_key = #{notifKey,jdbcType=VARCHAR}
and p.text_value = 'true'
and p.resource_id = c.id
WHERE
u.email is not null
</if>

</select>


+ 139
- 7
server/sonar-db-dao/src/test/java/org/sonar/db/property/PropertiesDaoTest.java View File

@@ -29,6 +29,8 @@ import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
@@ -47,6 +49,7 @@ import org.sonar.db.user.UserDto;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet;
import static java.util.Collections.singletonList;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
import static org.mockito.Mockito.mock;
@@ -123,7 +126,8 @@ public class PropertiesDaoTest {
public void hasNotificationSubscribers() {
int userId1 = db.users().insertUser(u -> u.setLogin("user1")).getId();
int userId2 = db.users().insertUser(u -> u.setLogin("user2")).getId();
Long projectId = insertPrivateProject("PROJECT_A").getId();
String projectUuid = randomAlphabetic(8);
Long projectId = db.components().insertPrivateProject(db.getDefaultOrganization(), projectUuid).getId();
// global subscription
insertProperty("notification.DispatcherWithGlobalSubscribers.Email", "true", null, userId2);
// project subscription
@@ -134,26 +138,138 @@ public class PropertiesDaoTest {
insertProperty("notification.DispatcherWithGlobalAndProjectSubscribers.Email", "true", null, userId2);

// Nobody is subscribed
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers("PROJECT_A", singletonList("NotSexyDispatcher")))
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers(projectUuid, singletonList("NotSexyDispatcher")))
.isFalse();

// Global subscribers
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers("PROJECT_A", singletonList("DispatcherWithGlobalSubscribers")))
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers(projectUuid, singletonList("DispatcherWithGlobalSubscribers")))
.isTrue();

// Project subscribers
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers("PROJECT_A", singletonList("DispatcherWithProjectSubscribers")))
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers(projectUuid, singletonList("DispatcherWithProjectSubscribers")))
.isTrue();
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers("PROJECT_B", singletonList("DispatcherWithProjectSubscribers")))
.isFalse();

// Global + Project subscribers
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers("PROJECT_A", singletonList("DispatcherWithGlobalAndProjectSubscribers")))
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers(projectUuid, singletonList("DispatcherWithGlobalAndProjectSubscribers")))
.isTrue();
assertThat(underTest.hasProjectNotificationSubscribersForDispatchers("PROJECT_B", singletonList("DispatcherWithGlobalAndProjectSubscribers")))
.isTrue();
}

@Test
public void findEmailRecipientsForNotification_returns_empty_on_empty_properties_table() {
db.users().insertUser();
String dispatcherKey = randomAlphabetic(5);
String channelKey = randomAlphabetic(6);
String projectKey = randomAlphabetic(7);

Set<EmailSubscriberDto> subscribers = underTest.findEmailSubscribersForNotification(db.getSession(), dispatcherKey, channelKey, projectKey);

assertThat(subscribers).isEmpty();
}

@Test
public void findEmailRecipientsForNotification_finds_only_globally_subscribed_users_if_projectKey_is_null() {
int userId1 = db.users().insertUser(withEmail("user1")).getId();
int userId2 = db.users().insertUser(withEmail("user2")).getId();
int userId3 = db.users().insertUser(withEmail("user3")).getId();
int userId4 = db.users().insertUser(withEmail("user4")).getId();
long projectId = insertPrivateProject("PROJECT_A").getId();
String dispatcherKey = randomAlphabetic(5);
String otherDispatcherKey = randomAlphabetic(6);
String channelKey = randomAlphabetic(7);
String otherChannelKey = randomAlphabetic(8);
// user1 subscribed only globally
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", null, userId1);
// user2 subscribed on project and globally
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", null, userId2);
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", projectId, userId2);
// user3 subscribed on project only
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", projectId, userId3);
// user4 did not subscribe
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "false", projectId, userId4);

assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), dispatcherKey, channelKey, null))
.containsOnly(new EmailSubscriberDto("user1", true, emailOf("user1")), new EmailSubscriberDto("user2", true, emailOf("user2")));

assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), dispatcherKey, otherChannelKey, null))
.isEmpty();
assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), otherDispatcherKey, channelKey, null))
.isEmpty();
assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), channelKey, dispatcherKey, null))
.isEmpty();
}

@Test
public void findEmailRecipientsForNotification_finds_global_and_project_subscribed_users_when_projectKey_is_non_null() {
int userId1 = db.users().insertUser(withEmail("user1")).getId();
int userId2 = db.users().insertUser(withEmail("user2")).getId();
int userId3 = db.users().insertUser(withEmail("user3")).getId();
int userId4 = db.users().insertUser(withEmail("user4")).getId();
String projectKey = randomAlphabetic(3);
String otherProjectKey = randomAlphabetic(4);
long projectId = insertPrivateProject(projectKey).getId();
String dispatcherKey = randomAlphabetic(5);
String otherDispatcherKey = randomAlphabetic(6);
String channelKey = randomAlphabetic(7);
String otherChannelKey = randomAlphabetic(8);
// user1 subscribed only globally
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", null, userId1);
// user2 subscribed on project and globally
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", null, userId2);
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", projectId, userId2);
// user3 subscribed on project only
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", projectId, userId3);
// user4 did not subscribe
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "false", projectId, userId4);

assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), dispatcherKey, channelKey, projectKey))
.containsOnly(
new EmailSubscriberDto("user1", true, emailOf("user1")),
new EmailSubscriberDto("user2", true, emailOf("user2")), new EmailSubscriberDto("user2", false, "user2@foo"),
new EmailSubscriberDto("user3", false, emailOf("user3")));

assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), dispatcherKey, channelKey, otherProjectKey))
.containsOnly(
new EmailSubscriberDto("user1", true, emailOf("user1")),
new EmailSubscriberDto("user2", true, emailOf("user2")));
assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), dispatcherKey, otherChannelKey, otherProjectKey))
.isEmpty();
assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), otherDispatcherKey, channelKey, otherProjectKey))
.isEmpty();
}

@Test
public void findEmailRecipientsForNotification_ignores_subscribed_users_without_email() {
int userId1 = db.users().insertUser(withEmail("user1")).getId();
int userId2 = db.users().insertUser(noEmail("user2")).getId();
int userId3 = db.users().insertUser(withEmail("user3")).getId();
int userId4 = db.users().insertUser(noEmail("user4")).getId();
String projectKey = randomAlphabetic(3);
long projectId = insertPrivateProject(projectKey).getId();
String dispatcherKey = randomAlphabetic(4);
String channelKey = randomAlphabetic(5);
// user1 and user2 subscribed on project and globally
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", null, userId1);
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", projectId, userId1);
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", null, userId2);
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", projectId, userId2);
// user3 and user4 subscribed only globally
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", null, userId3);
insertProperty(propertyKeyOf(dispatcherKey, channelKey), "true", null, userId4);

assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), dispatcherKey, channelKey, projectKey))
.containsOnly(
new EmailSubscriberDto("user1", true, emailOf("user1")), new EmailSubscriberDto("user1", false, emailOf("user1")),
new EmailSubscriberDto("user3", true, emailOf("user3")));
assertThat(underTest.findEmailSubscribersForNotification(db.getSession(), dispatcherKey, channelKey, null))
.containsOnly(
new EmailSubscriberDto("user1", true, emailOf("user1")),
new EmailSubscriberDto("user3", true, emailOf("user3")));
}

@Test
public void selectGlobalProperties() {
// global
@@ -1017,8 +1133,24 @@ public class PropertiesDaoTest {
" and resource_id" + (resourceId == null ? " is null" : "='" + resourceId + "'")).get("id");
}

private ComponentDto insertPrivateProject(String uuid) {
return db.components().insertPrivateProject(db.getDefaultOrganization(), uuid);
private ComponentDto insertPrivateProject(String projectKey) {
return db.components().insertPrivateProject(db.getDefaultOrganization(), t -> t.setDbKey(projectKey));
}

private static Consumer<UserDto> withEmail(String login) {
return u -> u.setLogin(login).setEmail(emailOf(login));
}

private static String emailOf(String login) {
return login + "@foo";
}

private static Consumer<UserDto> noEmail(String login) {
return u -> u.setLogin(login).setEmail(null);
}

private static String propertyKeyOf(String dispatcherKey, String channelKey) {
return String.format("notification.%s.%s", dispatcherKey, channelKey);
}

private static PropertyDtoAssert assertThatDto(@Nullable PropertyDto dto) {

+ 11
- 0
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotification.java View File

@@ -19,6 +19,7 @@
*/
package org.sonar.server.issue.notification;

import javax.annotation.CheckForNull;
import org.sonar.api.utils.Durations;
import org.sonar.db.DbClient;
import org.sonar.db.user.UserDto;
@@ -43,6 +44,16 @@ public class MyNewIssuesNotification extends NewIssuesNotification {
return this;
}

@CheckForNull
public String getProjectKey() {
return getFieldValue("projectKey");
}

@CheckForNull
public String getAssignee() {
return getFieldValue(FIELD_ASSIGNEE);
}

@Override
public boolean equals(Object obj) {
return super.equals(obj);

+ 0
- 69
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcher.java View File

@@ -1,69 +0,0 @@
/*
* 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.issue.notification;

import com.google.common.collect.Multimap;
import java.util.Collection;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.server.notification.NotificationDispatcher;
import org.sonar.server.notification.NotificationDispatcherMetadata;
import org.sonar.server.notification.NotificationManager;

import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;

/**
* This dispatcher means: "notify me when new issues are introduced during project analysis"
*/
public class MyNewIssuesNotificationDispatcher extends NotificationDispatcher {

public static final String KEY = "SQ-MyNewIssues";
private final NotificationManager manager;

public MyNewIssuesNotificationDispatcher(NotificationManager manager) {
super(MyNewIssuesNotification.MY_NEW_ISSUES_NOTIF_TYPE);
this.manager = manager;
}

public static NotificationDispatcherMetadata newMetadata() {
return NotificationDispatcherMetadata.create(KEY)
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true));
}

@Override
public String getKey() {
return KEY;
}

@Override
public void dispatch(Notification notification, Context context) {
String projectKey = notification.getFieldValue("projectKey");
String assignee = notification.getFieldValue("assignee");
Multimap<String, NotificationChannel> subscribedRecipients = manager
.findSubscribedRecipientsForDispatcher(this, projectKey, ALL_MUST_HAVE_ROLE_USER);

Collection<NotificationChannel> channels = subscribedRecipients.get(assignee);
for (NotificationChannel channel : channels) {
context.addUser(assignee, channel);
}
}

}

+ 108
- 0
server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MyNewIssuesNotificationHandler.java View File

@@ -0,0 +1,108 @@
/*
* 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.issue.notification;

import com.google.common.collect.Multimap;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.server.notification.NotificationDispatcherMetadata;
import org.sonar.server.notification.NotificationHandler;
import org.sonar.server.notification.NotificationManager;
import org.sonar.server.notification.email.EmailNotificationChannel;

import static org.sonar.core.util.stream.MoreCollectors.index;
import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;

public class MyNewIssuesNotificationHandler implements NotificationHandler<MyNewIssuesNotification> {
public static final String KEY = "SQ-MyNewIssues";

private final NotificationManager notificationManager;
private final EmailNotificationChannel emailNotificationChannel;

public MyNewIssuesNotificationHandler(NotificationManager notificationManager, EmailNotificationChannel emailNotificationChannel) {
this.notificationManager = notificationManager;
this.emailNotificationChannel = emailNotificationChannel;
}

public static NotificationDispatcherMetadata newMetadata() {
return NotificationDispatcherMetadata.create(KEY)
.setProperty(GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(PER_PROJECT_NOTIFICATION, String.valueOf(true));
}

@Override
public Class<MyNewIssuesNotification> getNotificationClass() {
return MyNewIssuesNotification.class;
}

@Override
public int deliver(Collection<MyNewIssuesNotification> notifications) {
if (notifications.isEmpty() || !emailNotificationChannel.isActivated()) {
return 0;
}

Multimap<String, MyNewIssuesNotification> notificationsByProjectKey = notifications.stream()
.filter(t -> t.getProjectKey() != null)
.filter(t -> t.getAssignee() != null)
.collect(index(MyNewIssuesNotification::getProjectKey));
if (notificationsByProjectKey.isEmpty()) {
return 0;
}

Set<EmailNotificationChannel.EmailDeliveryRequest> deliveryRequests = notificationsByProjectKey.asMap().entrySet()
.stream()
.flatMap(e -> toEmailDeliveryRequests(e.getKey(), e.getValue()))
.collect(MoreCollectors.toSet(notifications.size()));
if (deliveryRequests.isEmpty()) {
return 0;
}
return emailNotificationChannel.deliver(deliveryRequests);
}

private Stream<? extends EmailNotificationChannel.EmailDeliveryRequest> toEmailDeliveryRequests(String projectKey, Collection<MyNewIssuesNotification> notifications) {
Map<String, NotificationManager.EmailRecipient> recipientsByLogin = notificationManager
.findSubscribedEmailRecipients(KEY, projectKey, ALL_MUST_HAVE_ROLE_USER)
.stream()
.collect(MoreCollectors.uniqueIndex(NotificationManager.EmailRecipient::getLogin));
return notifications.stream()
.map(notification -> toEmailDeliveryRequest(recipientsByLogin, notification))
.filter(Objects::nonNull);
}

@CheckForNull
private static EmailNotificationChannel.EmailDeliveryRequest toEmailDeliveryRequest(Map<String, NotificationManager.EmailRecipient> recipientsByLogin,
MyNewIssuesNotification notification) {
String assignee = notification.getAssignee();

NotificationManager.EmailRecipient emailRecipient = recipientsByLogin.get(assignee);
if (emailRecipient != null) {
return new EmailNotificationChannel.EmailDeliveryRequest(emailRecipient.getEmail(), notification);
}
return null;
}

}

+ 54
- 1
server/sonar-server-common/src/main/java/org/sonar/server/notification/DefaultNotificationManager.java View File

@@ -38,11 +38,15 @@ import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.utils.SonarException;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.notification.NotificationQueueDto;
import org.sonar.db.property.EmailSubscriberDto;
import org.sonar.db.property.Subscriber;
import org.sonar.server.notification.email.EmailNotificationChannel;

import static java.util.Collections.emptySet;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;

@@ -120,7 +124,7 @@ public class DefaultNotificationManager implements NotificationManager {
@Override
public Multimap<String, NotificationChannel> findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher,
String projectKey, SubscriberPermissionsOnProject subscriberPermissionsOnProject) {
requireNonNull(projectKey, "projectKey is mandatory");
verifyProjectKey(projectKey);
String dispatcherKey = dispatcher.getKey();

Set<SubscriberAndChannel> subscriberAndChannels = Arrays.stream(notificationChannels)
@@ -141,6 +145,10 @@ public class DefaultNotificationManager implements NotificationManager {
return builder.build();
}

private static void verifyProjectKey(String projectKey) {
requireNonNull(projectKey, "projectKey is mandatory");
}

private Stream<SubscriberAndChannel> toSubscriberAndChannels(String dispatcherKey, String projectKey, NotificationChannel notificationChannel) {
Set<Subscriber> usersForNotification = dbClient.propertiesDao().findUsersForNotification(dispatcherKey, notificationChannel.getKey(), projectKey);
return usersForNotification
@@ -173,6 +181,51 @@ public class DefaultNotificationManager implements NotificationManager {
return dbClient.authorizationDao().keepAuthorizedLoginsOnProject(dbSession, logins, projectKey, permission);
}

@Override
public Set<EmailRecipient> findSubscribedEmailRecipients(String dispatcherKey, String projectKey, SubscriberPermissionsOnProject subscriberPermissionsOnProject) {
verifyProjectKey(projectKey);

try (DbSession dbSession = dbClient.openSession(false)) {
Set<EmailSubscriberDto> emailSubscribers = dbClient.propertiesDao().findEmailSubscribersForNotification(
dbSession, dispatcherKey, EmailNotificationChannel.class.getSimpleName(), projectKey);
if (emailSubscribers.isEmpty()) {
return emptySet();
}

return keepAuthorizedEmailSubscribers(dbSession, projectKey, emailSubscribers, subscriberPermissionsOnProject)
.map(emailSubscriber -> new EmailRecipient(emailSubscriber.getLogin(), emailSubscriber.getEmail()))
.collect(MoreCollectors.toSet());
}
}

private Stream<EmailSubscriberDto> keepAuthorizedEmailSubscribers(DbSession dbSession, String projectKey, Set<EmailSubscriberDto> emailSubscribers,
SubscriberPermissionsOnProject requiredPermissions) {
if (requiredPermissions.getGlobalSubscribers().equals(requiredPermissions.getProjectSubscribers())) {
return keepAuthorizedEmailSubscribers(dbSession, projectKey, emailSubscribers, null, requiredPermissions.getGlobalSubscribers());
} else {
return Stream.concat(
keepAuthorizedEmailSubscribers(dbSession, projectKey, emailSubscribers, true, requiredPermissions.getGlobalSubscribers()),
keepAuthorizedEmailSubscribers(dbSession, projectKey, emailSubscribers, false, requiredPermissions.getProjectSubscribers()));
}
}

private Stream<EmailSubscriberDto> keepAuthorizedEmailSubscribers(DbSession dbSession, String projectKey, Set<EmailSubscriberDto> emailSubscribers,
@Nullable Boolean global, String permission) {
Set<EmailSubscriberDto> subscribers = emailSubscribers.stream()
.filter(s -> global == null || s.isGlobal() == global)
.collect(Collectors.toSet());
if (subscribers.isEmpty()) {
return Stream.empty();
}

Set<String> logins = subscribers.stream()
.map(EmailSubscriberDto::getLogin)
.collect(Collectors.toSet());
Set<String> authorizedLogins = dbClient.authorizationDao().keepAuthorizedLoginsOnProject(dbSession, logins, projectKey, permission);
return subscribers.stream()
.filter(s -> authorizedLogins.contains(s.getLogin()));
}

private static final class SubscriberAndChannel {
private final Subscriber subscriber;
private final NotificationChannel channel;

+ 33
- 0
server/sonar-server-common/src/main/java/org/sonar/server/notification/NotificationHandler.java View File

@@ -0,0 +1,33 @@
/*
* 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 java.util.Collection;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.notifications.Notification;
import org.sonar.api.server.ServerSide;

@ServerSide
@ComputeEngineSide
public interface NotificationHandler<T extends Notification> {
Class<T> getNotificationClass();

int deliver(Collection<T> notifications);
}

+ 47
- 2
server/sonar-server-common/src/main/java/org/sonar/server/notification/NotificationManager.java View File

@@ -21,6 +21,8 @@ package org.sonar.server.notification;

import com.google.common.collect.Multimap;
import java.util.Objects;
import java.util.Set;
import javax.annotation.concurrent.Immutable;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.web.UserRole;
@@ -53,14 +55,57 @@ public interface NotificationManager {
* </p>
*
* @param dispatcher the dispatcher for which this list of users is requested
* @param projectUuid UUID of the project
* @param projectKey key of the project
* @param subscriberPermissionsOnProject the required permission for global and project subscribers
*
* @return the list of user login along with the subscribed channels
*/
Multimap<String, NotificationChannel> findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher, String projectUuid,
Multimap<String, NotificationChannel> findSubscribedRecipientsForDispatcher(NotificationDispatcher dispatcher, String projectKey,
SubscriberPermissionsOnProject subscriberPermissionsOnProject);

@Immutable
final class EmailRecipient {
private final String login;
private final String email;

public EmailRecipient(String login, String email) {
this.login = requireNonNull(login, "login can't be null");
this.email = requireNonNull(email, "email can't be null");
}

public String getLogin() {
return login;
}

public String getEmail() {
return email;
}

@Override
public String toString() {
return "EmailRecipient{" + "'" + login + '\'' + ":'" + email + '\'' + '}';
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EmailRecipient that = (EmailRecipient) o;
return login.equals(that.login) && email.equals(that.email);
}

@Override
public int hashCode() {
return Objects.hash(login, email);
}
}

Set<EmailRecipient> findSubscribedEmailRecipients(String dispatcherKey, String projectKey, SubscriberPermissionsOnProject subscriberPermissionsOnProject);

final class SubscriberPermissionsOnProject {
public static final SubscriberPermissionsOnProject ALL_MUST_HAVE_ROLE_USER = new SubscriberPermissionsOnProject(UserRole.USER);


+ 55
- 3
server/sonar-server-common/src/main/java/org/sonar/server/notification/NotificationService.java View File

@@ -29,6 +29,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.notifications.Notification;
@@ -38,6 +39,8 @@ import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.db.DbClient;

import static com.google.common.base.Preconditions.checkArgument;

@ServerSide
@ComputeEngineSide
public class NotificationService {
@@ -45,21 +48,70 @@ public class NotificationService {
private static final Logger LOG = Loggers.get(NotificationService.class);

private final List<NotificationDispatcher> dispatchers;
private final List<NotificationHandler> handlers;
private final DbClient dbClient;

public NotificationService(DbClient dbClient, NotificationDispatcher[] dispatchers) {
public NotificationService(DbClient dbClient, NotificationDispatcher[] dispatchers, NotificationHandler[] handlers) {
this.dbClient = dbClient;
this.dispatchers = ImmutableList.copyOf(dispatchers);
this.handlers = ImmutableList.copyOf(handlers);
}

/**
* Default constructor when no dispatchers.
* Used by Pico when there are no handler nor dispatcher.
*/
public NotificationService(DbClient dbClient) {
this(dbClient, new NotificationDispatcher[0]);
this(dbClient, new NotificationDispatcher[0], new NotificationHandler[0]);
}

/**
* Used by Pico when there are no dispatcher.
*/
public NotificationService(DbClient dbClient, NotificationHandler[] handlers) {
this(dbClient, new NotificationDispatcher[0], handlers);
}

/**
* Used by Pico when there are no handler.
*/
public NotificationService(DbClient dbClient, NotificationDispatcher[] dispatchers) {
this(dbClient, dispatchers, new NotificationHandler[0]);
}

public <T extends Notification> int deliverEmails(Collection<T> notifications) {
if (handlers.isEmpty()) {
return 0;
}

Class<T> aClass = typeClassOf(notifications);
if (aClass == null) {
return 0;
}

checkArgument(aClass != Notification.class, "Type of notification objects must be a subtype of " + Notification.class.getSimpleName());
return handlers.stream()
.filter(t -> t.getNotificationClass() == aClass)
.findFirst()
.map(t -> (NotificationHandler<T>) t)
.map(handler -> handler.deliver(notifications))
.orElse(0);
}

@SuppressWarnings("unchecked")
@CheckForNull
private static <T extends Notification> Class<T> typeClassOf(Collection<T> collection) {
if (collection.isEmpty()) {
return null;
}

return (Class<T>) collection.iterator().next().getClass();
}

public int deliver(Notification notification) {
if (dispatchers.isEmpty()) {
return 0;
}

SetMultimap<String, NotificationChannel> recipients = HashMultimap.create();
for (NotificationDispatcher dispatcher : dispatchers) {
NotificationDispatcher.Context context = new ContextImpl(recipients);

+ 75
- 4
server/sonar-server-common/src/main/java/org/sonar/server/notification/email/EmailNotificationChannel.java View File

@@ -21,7 +21,10 @@ package org.sonar.server.notification.email;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Objects;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.concurrent.Immutable;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.SimpleEmail;
@@ -38,6 +41,8 @@ import org.sonar.db.user.UserDto;
import org.sonar.plugins.emailnotifications.api.EmailMessage;
import org.sonar.plugins.emailnotifications.api.EmailTemplate;

import static java.util.Objects.requireNonNull;

/**
* References:
* <ul>
@@ -83,6 +88,7 @@ public class EmailNotificationChannel extends NotificationChannel {
private static final String REFERENCES_HEADER = "References";

private static final String SUBJECT_DEFAULT = "Notification";
private static final String SMTP_HOST_NOT_CONFIGURED_DEBUG_MSG = "SMTP host was not configured - email will not be sent";

private final EmailSettings configuration;
private final EmailTemplate[] templates;
@@ -94,10 +100,14 @@ public class EmailNotificationChannel extends NotificationChannel {
this.dbClient = dbClient;
}

public boolean isActivated() {
return !StringUtils.isBlank(configuration.getSmtpHost());
}

@Override
public boolean deliver(Notification notification, String username) {
if (StringUtils.isBlank(configuration.getSmtpHost())) {
LOG.debug("SMTP host was not configured - email will not be sent");
if (!isActivated()) {
LOG.debug(SMTP_HOST_NOT_CONFIGURED_DEBUG_MSG);
return false;
}

@@ -115,6 +125,67 @@ public class EmailNotificationChannel extends NotificationChannel {
return false;
}

@Immutable
public static final class EmailDeliveryRequest {
private final String recipientEmail;
private final Notification notification;

public EmailDeliveryRequest(String recipientEmail, Notification notification) {
this.recipientEmail = requireNonNull(recipientEmail, "recipientEmail can't be null");
this.notification = requireNonNull(notification, "notification can't be null");
}

public String getRecipientEmail() {
return recipientEmail;
}

public Notification getNotification() {
return notification;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EmailDeliveryRequest that = (EmailDeliveryRequest) o;
return Objects.equals(recipientEmail, that.recipientEmail) &&
Objects.equals(notification, that.notification);
}

@Override
public int hashCode() {
return Objects.hash(recipientEmail, notification);
}

@Override
public String toString() {
return "EmailDeliveryRequest{" + "'" + recipientEmail + '\'' + " : " + notification + '}';
}
}

public int deliver(Set<EmailDeliveryRequest> deliveries) {
if (!isActivated()) {
LOG.debug(SMTP_HOST_NOT_CONFIGURED_DEBUG_MSG);
return 0;
}

return (int) deliveries.stream()
.map(t -> {
EmailMessage emailMessage = format(t.getNotification());
if (emailMessage != null) {
emailMessage.setTo(t.getRecipientEmail());
return deliver(emailMessage);
}
return null;
})
.filter(Objects::nonNull)
.count();
}

@CheckForNull
private User findByLogin(String login) {
try (DbSession dbSession = dbClient.openSession(false)) {
@@ -135,8 +206,8 @@ public class EmailNotificationChannel extends NotificationChannel {
}

boolean deliver(EmailMessage emailMessage) {
if (StringUtils.isBlank(configuration.getSmtpHost())) {
LOG.debug("SMTP host was not configured - email will not be sent");
if (!isActivated()) {
LOG.debug(SMTP_HOST_NOT_CONFIGURED_DEBUG_MSG);
return false;
}
try {

+ 0
- 72
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationDispatcherTest.java View File

@@ -1,72 +0,0 @@
/*
* 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.issue.notification;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.api.web.UserRole;
import org.sonar.server.notification.NotificationDispatcher;
import org.sonar.server.notification.NotificationManager;

import static org.mockito.Mockito.*;

public class MyNewIssuesNotificationDispatcherTest {

private MyNewIssuesNotificationDispatcher underTest;

private NotificationManager notificationManager = mock(NotificationManager.class);
private NotificationDispatcher.Context context = mock(NotificationDispatcher.Context.class);
private NotificationChannel emailChannel = mock(NotificationChannel.class);
private NotificationChannel twitterChannel = mock(NotificationChannel.class);


@Before
public void setUp() {
underTest = new MyNewIssuesNotificationDispatcher(notificationManager);
}

@Test
public void do_not_dispatch_if_no_new_notification() {
Notification notification = new Notification("other-notif");
underTest.performDispatch(notification, context);

verify(context, never()).addUser(any(String.class), any(NotificationChannel.class));
}

@Test
public void dispatch_to_users_who_have_subscribed_to_notification_and_project() {
Multimap<String, NotificationChannel> recipients = HashMultimap.create();
recipients.put("user1", emailChannel);
recipients.put("user2", twitterChannel);
when(notificationManager.findSubscribedRecipientsForDispatcher(underTest, "struts", new NotificationManager.SubscriberPermissionsOnProject(UserRole.USER))).thenReturn(recipients);

Notification notification = new Notification(MyNewIssuesNotification.MY_NEW_ISSUES_NOTIF_TYPE)
.setFieldValue("projectKey", "struts")
.setFieldValue("assignee", "user1");
underTest.performDispatch(notification, context);

verify(context).addUser("user1", emailChannel);
verifyNoMoreInteractions(context);
}
}

+ 355
- 0
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationHandlerTest.java View File

@@ -0,0 +1,355 @@
/*
* 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.issue.notification;

import com.google.common.collect.ImmutableSet;
import java.util.Collections;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.junit.Test;
import org.mockito.Mockito;
import org.sonar.server.notification.NotificationDispatcherMetadata;
import org.sonar.server.notification.NotificationManager;
import org.sonar.server.notification.NotificationManager.EmailRecipient;
import org.sonar.server.notification.email.EmailNotificationChannel;
import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;

import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.server.notification.NotificationDispatcherMetadata.GLOBAL_NOTIFICATION;
import static org.sonar.server.notification.NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION;
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;

public class MyNewIssuesNotificationHandlerTest {
private static final String MY_NEW_ISSUES_DISPATCHER_KEY = "SQ-MyNewIssues";
private NotificationManager notificationManager = mock(NotificationManager.class);
private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);

private MyNewIssuesNotificationHandler underTest = new MyNewIssuesNotificationHandler(notificationManager, emailNotificationChannel);

@Test
public void verify_myNewIssues_notification_dispatcher_key() {
NotificationDispatcherMetadata metadata = MyNewIssuesNotificationHandler.newMetadata();

assertThat(metadata.getDispatcherKey()).isEqualTo(MY_NEW_ISSUES_DISPATCHER_KEY);
}

@Test
public void myNewIssues_notification_is_enable_at_global_level() {
NotificationDispatcherMetadata metadata = MyNewIssuesNotificationHandler.newMetadata();

assertThat(metadata.getProperty(GLOBAL_NOTIFICATION)).isEqualTo("true");
}

@Test
public void myNewIssues_notification_is_enable_at_project_level() {
NotificationDispatcherMetadata metadata = MyNewIssuesNotificationHandler.newMetadata();

assertThat(metadata.getProperty(PER_PROJECT_NOTIFICATION)).isEqualTo("true");
}

@Test
public void getNotificationClass_is_MyNewIssuesNotification() {
assertThat(underTest.getNotificationClass()).isEqualTo(MyNewIssuesNotification.class);
}

@Test
public void deliver_has_no_effect_if_notifications_is_empty() {
when(emailNotificationChannel.isActivated()).thenReturn(true);
int deliver = underTest.deliver(Collections.emptyList());

assertThat(deliver).isZero();
verifyZeroInteractions(notificationManager, emailNotificationChannel);
}

@Test
public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() {
when(emailNotificationChannel.isActivated()).thenReturn(false);
Set<MyNewIssuesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
.mapToObj(i -> mock(MyNewIssuesNotification.class))
.collect(toSet());

int deliver = underTest.deliver(notifications);

assertThat(deliver).isZero();
verifyZeroInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
notifications.forEach(Mockito::verifyZeroInteractions);
}

@Test
public void deliver_has_no_effect_if_no_notification_has_projectKey() {
when(emailNotificationChannel.isActivated()).thenReturn(true);
Set<MyNewIssuesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
.mapToObj(i -> newNotification(null, null))
.collect(toSet());

int deliver = underTest.deliver(notifications);

assertThat(deliver).isZero();
verifyZeroInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
notifications.forEach(notification -> {
verify(notification).getProjectKey();
verifyNoMoreInteractions(notification);
});
}

@Test
public void deliver_has_no_effect_if_no_notification_has_assignee() {
when(emailNotificationChannel.isActivated()).thenReturn(true);
Set<MyNewIssuesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10))
.mapToObj(i -> newNotification(randomAlphabetic(5 + i), null))
.collect(toSet());

int deliver = underTest.deliver(notifications);

assertThat(deliver).isZero();
verifyZeroInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
notifications.forEach(notification -> {
verify(notification).getProjectKey();
verify(notification).getAssignee();
verifyNoMoreInteractions(notification);
});
}

@Test
public void deliver_has_no_effect_if_no_notification_has_subscribed_assignee_to_MyNewIssue_notifications() {
String projectKey = randomAlphabetic(12);
String assignee = randomAlphabetic(10);
MyNewIssuesNotification notification = newNotification(projectKey, assignee);
when(emailNotificationChannel.isActivated()).thenReturn(true);
when(notificationManager.findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER))
.thenReturn(emptySet());

int deliver = underTest.deliver(Collections.singleton(notification));

assertThat(deliver).isZero();
verify(notificationManager).findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER);
verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
}

@Test
public void deliver_ignores_notification_without_projectKey() {
String projectKey = randomAlphabetic(10);
Set<MyNewIssuesNotification> withProjectKey = IntStream.range(0, 1 + new Random().nextInt(5))
.mapToObj(i -> newNotification(projectKey, randomAlphabetic(11 + i)))
.collect(toSet());
Set<MyNewIssuesNotification> noProjectKey = IntStream.range(0, 1 + new Random().nextInt(5))
.mapToObj(i -> newNotification(null, randomAlphabetic(11 + i)))
.collect(toSet());
Set<MyNewIssuesNotification> noProjectKeyNoAssignee = randomSetOfNotifications(null, null);
Set<EmailRecipient> authorizedRecipients = withProjectKey.stream()
.map(n -> new EmailRecipient(n.getAssignee(), n.getAssignee() + "@foo"))
.collect(toSet());
Set<EmailDeliveryRequest> expectedRequests = withProjectKey.stream()
.map(n -> new EmailDeliveryRequest(n.getAssignee() + "@foo", n))
.collect(toSet());
when(emailNotificationChannel.isActivated()).thenReturn(true);
when(notificationManager.findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER))
.thenReturn(authorizedRecipients);

Set<MyNewIssuesNotification> notifications = Stream.of(withProjectKey.stream(), noProjectKey.stream(), noProjectKeyNoAssignee.stream())
.flatMap(t -> t)
.collect(toSet());
int deliver = underTest.deliver(notifications);

assertThat(deliver).isZero();
verify(notificationManager).findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER);
verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verify(emailNotificationChannel).deliver(expectedRequests);
verifyNoMoreInteractions(emailNotificationChannel);
}

@Test
public void deliver_ignores_notification_without_assignee() {
String projectKey = randomAlphabetic(10);
Set<MyNewIssuesNotification> withAssignee = IntStream.range(0, 1 + new Random().nextInt(5))
.mapToObj(i -> newNotification(projectKey, randomAlphabetic(11 + i)))
.collect(toSet());
Set<MyNewIssuesNotification> noAssignee = randomSetOfNotifications(projectKey, null);
Set<MyNewIssuesNotification> noProjectKeyNoAssignee = randomSetOfNotifications(null, null);
Set<EmailRecipient> authorizedRecipients = withAssignee.stream()
.map(n -> new EmailRecipient(n.getAssignee(), n.getAssignee() + "@foo"))
.collect(toSet());
Set<EmailDeliveryRequest> expectedRequests = withAssignee.stream()
.map(n -> new EmailDeliveryRequest(n.getAssignee() + "@foo", n))
.collect(toSet());
when(emailNotificationChannel.isActivated()).thenReturn(true);
when(notificationManager.findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER))
.thenReturn(authorizedRecipients);

Set<MyNewIssuesNotification> notifications = Stream.of(withAssignee.stream(), noAssignee.stream(), noProjectKeyNoAssignee.stream())
.flatMap(t -> t)
.collect(toSet());
int deliver = underTest.deliver(notifications);

assertThat(deliver).isZero();
verify(notificationManager).findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER);
verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verify(emailNotificationChannel).deliver(expectedRequests);
verifyNoMoreInteractions(emailNotificationChannel);
}

@Test
public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_MyNewIssue_notifications() {
String projectKey1 = randomAlphabetic(10);
String assignee1 = randomAlphabetic(11);
String projectKey2 = randomAlphabetic(12);
String assignee2 = randomAlphabetic(13);
Set<MyNewIssuesNotification> notifications1 = randomSetOfNotifications(projectKey1, assignee1);
Set<MyNewIssuesNotification> notifications2 = randomSetOfNotifications(projectKey2, assignee2);
when(emailNotificationChannel.isActivated()).thenReturn(true);
when(notificationManager.findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey1, ALL_MUST_HAVE_ROLE_USER))
.thenReturn(emptySet());
when(notificationManager.findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey2, ALL_MUST_HAVE_ROLE_USER))
.thenReturn(emptySet());

int deliver = underTest.deliver(Stream.concat(notifications1.stream(), notifications2.stream()).collect(toSet()));

assertThat(deliver).isZero();
verify(notificationManager).findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey1, ALL_MUST_HAVE_ROLE_USER);
verify(notificationManager).findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey2, ALL_MUST_HAVE_ROLE_USER);
verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verifyNoMoreInteractions(emailNotificationChannel);
}

@Test
public void deliver_ignores_notifications_which_assignee_has_no_subscribed_to_MyNewIssue_notifications() {
String projectKey = randomAlphabetic(5);
String assignee1 = randomAlphabetic(6);
String assignee2 = randomAlphabetic(7);
String assignee3 = randomAlphabetic(8);
// assignee1 is not authorized
Set<MyNewIssuesNotification> assignee1Notifications = randomSetOfNotifications(projectKey, assignee1);
// assignee2 is authorized
Set<MyNewIssuesNotification> assignee2Notifications = randomSetOfNotifications(projectKey, assignee2);
// assignee 3 is authorized but has no notification
when(emailNotificationChannel.isActivated()).thenReturn(true);
when(notificationManager.findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER))
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee2), emailRecipientOf(assignee3)));
Set<EmailDeliveryRequest> expectedRequests = assignee2Notifications.stream()
.map(t -> new EmailDeliveryRequest(emailOf(t.getAssignee()), t))
.collect(toSet());
int deliveredCount = new Random().nextInt(expectedRequests.size());
when(emailNotificationChannel.deliver(expectedRequests)).thenReturn(deliveredCount);

int deliver = underTest.deliver(Stream.concat(assignee1Notifications.stream(), assignee2Notifications.stream()).collect(toSet()));

assertThat(deliver).isEqualTo(deliveredCount);
verify(notificationManager).findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER);
verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verify(emailNotificationChannel).deliver(expectedRequests);
verifyNoMoreInteractions(emailNotificationChannel);
}

@Test
public void deliver_returns_sum_of_delivery_counts_when_multiple_projects() {
String projectKey1 = randomAlphabetic(5);
String projectKey2 = randomAlphabetic(6);
String projectKey3 = randomAlphabetic(7);
String assignee1 = randomAlphabetic(8);
String assignee2 = randomAlphabetic(9);
String assignee3 = randomAlphabetic(10);
// assignee1 has subscribed to project1 only, no notification on project3
Set<MyNewIssuesNotification> assignee1Project1 = randomSetOfNotifications(projectKey1, assignee1);
Set<MyNewIssuesNotification> assignee1Project2 = randomSetOfNotifications(projectKey2, assignee1);
// assignee2 is subscribed to project1 and project2, notifications on all projects
Set<MyNewIssuesNotification> assignee2Project1 = randomSetOfNotifications(projectKey1, assignee2);
Set<MyNewIssuesNotification> assignee2Project2 = randomSetOfNotifications(projectKey2, assignee2);
Set<MyNewIssuesNotification> assignee2Project3 = randomSetOfNotifications(projectKey3, assignee2);
// assignee3 is subscribed to project2 only, no notification on project1
Set<MyNewIssuesNotification> assignee3Project2 = randomSetOfNotifications(projectKey2, assignee3);
Set<MyNewIssuesNotification> assignee3Project3 = randomSetOfNotifications(projectKey3, assignee3);
when(emailNotificationChannel.isActivated()).thenReturn(true);
when(notificationManager.findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey1, ALL_MUST_HAVE_ROLE_USER))
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee1), emailRecipientOf(assignee2)));
when(notificationManager.findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey2, ALL_MUST_HAVE_ROLE_USER))
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee2), emailRecipientOf(assignee3)));
when(notificationManager.findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey3, ALL_MUST_HAVE_ROLE_USER))
.thenReturn(emptySet());
Set<EmailDeliveryRequest> expectedRequests = Stream.of(
assignee1Project1.stream(), assignee2Project1.stream(), assignee2Project2.stream(), assignee3Project2.stream())
.flatMap(t -> t)
.map(t -> new EmailDeliveryRequest(emailOf(t.getAssignee()), t))
.collect(toSet());
int deliveredCount = new Random().nextInt(expectedRequests.size());
when(emailNotificationChannel.deliver(expectedRequests)).thenReturn(deliveredCount);

Set<MyNewIssuesNotification> notifications = Stream.of(
assignee1Project1.stream(), assignee1Project2.stream(),
assignee2Project1.stream(), assignee2Project2.stream(),
assignee2Project3.stream(), assignee3Project2.stream(), assignee3Project3.stream())
.flatMap(t -> t)
.collect(toSet());
int deliver = underTest.deliver(notifications);

assertThat(deliver).isEqualTo(deliveredCount);
verify(notificationManager).findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey1, ALL_MUST_HAVE_ROLE_USER);
verify(notificationManager).findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey2, ALL_MUST_HAVE_ROLE_USER);
verify(notificationManager).findSubscribedEmailRecipients(MY_NEW_ISSUES_DISPATCHER_KEY, projectKey3, ALL_MUST_HAVE_ROLE_USER);
verifyNoMoreInteractions(notificationManager);
verify(emailNotificationChannel).isActivated();
verify(emailNotificationChannel).deliver(expectedRequests);
verifyNoMoreInteractions(emailNotificationChannel);
}

private static Set<MyNewIssuesNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String assignee) {
return IntStream.range(0, 1 + new Random().nextInt(5))
.mapToObj(i -> newNotification(projectKey, assignee))
.collect(Collectors.toSet());
}

private static MyNewIssuesNotification newNotification(@Nullable String projectKey, @Nullable String assignee) {
MyNewIssuesNotification notification = mock(MyNewIssuesNotification.class);
when(notification.getProjectKey()).thenReturn(projectKey);
when(notification.getAssignee()).thenReturn(assignee);
return notification;
}

private static EmailRecipient emailRecipientOf(String assignee1) {
return new EmailRecipient(assignee1, emailOf(assignee1));
}

private static String emailOf(String assignee1) {
return assignee1 + "@bar";
}
}

+ 21
- 2
server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MyNewIssuesNotificationTest.java View File

@@ -25,13 +25,14 @@ import org.sonar.db.DbClient;
import org.sonar.db.user.UserDto;
import org.sonar.db.user.UserTesting;

import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_ASSIGNEE;

public class MyNewIssuesNotificationTest {

MyNewIssuesNotification underTest = new MyNewIssuesNotification(mock(DbClient.class), mock(Durations.class));
private MyNewIssuesNotification underTest = new MyNewIssuesNotification(mock(DbClient.class), mock(Durations.class));

@Test
public void set_assignee() {
@@ -39,11 +40,29 @@ public class MyNewIssuesNotificationTest {

underTest.setAssignee(user);

assertThat(underTest.getFieldValue(FIELD_ASSIGNEE)).isEqualTo(user.getLogin());
assertThat(underTest.getFieldValue(FIELD_ASSIGNEE))
.isEqualTo(underTest.getAssignee())
.isEqualTo(user.getLogin());
}

@Test
public void set_with_a_specific_type() {
assertThat(underTest.getType()).isEqualTo(MyNewIssuesNotification.MY_NEW_ISSUES_NOTIF_TYPE);
}

@Test
public void getProjectKey_returns_null_if_setProject_has_no_been_called() {
assertThat(underTest.getProjectKey()).isNull();
}

@Test
public void getProjectKey_returns_projectKey_if_setProject_has_been_called() {
String projectKey = randomAlphabetic(5);
String projectName = randomAlphabetic(6);
String branchName = randomAlphabetic(7);
String pullRequest = randomAlphabetic(8);
underTest.setProject(projectKey, projectName, branchName, pullRequest);

assertThat(underTest.getProjectKey()).isEqualTo(projectKey);
}
}

+ 168
- 45
server/sonar-server-common/src/test/java/org/sonar/server/notification/DefaultNotificationManagerTest.java View File

@@ -19,15 +19,22 @@
*/
package org.sonar.server.notification;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import java.io.InvalidClassException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.RandomStringUtils;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.InOrder;
import org.sonar.api.notifications.Notification;
import org.sonar.api.notifications.NotificationChannel;
@@ -37,11 +44,15 @@ import org.sonar.db.DbSession;
import org.sonar.db.notification.NotificationQueueDao;
import org.sonar.db.notification.NotificationQueueDto;
import org.sonar.db.permission.AuthorizationDao;
import org.sonar.db.property.EmailSubscriberDto;
import org.sonar.db.property.PropertiesDao;
import org.sonar.db.property.Subscriber;
import org.sonar.server.notification.NotificationManager.EmailRecipient;
import org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject;

import static com.google.common.collect.Sets.newHashSet;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -55,9 +66,13 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.internal.verification.VerificationModeFactory.times;
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER;

public class DefaultNotificationManagerTest {

@Rule
public ExpectedException expectedException = ExpectedException.none();

private DefaultNotificationManager underTest;

private PropertiesDao propertiesDao = mock(PropertiesDao.class);
@@ -135,21 +150,22 @@ public class DefaultNotificationManagerTest {

@Test
public void shouldFindSubscribedRecipientForGivenResource() {
String projectUuid = "uuid_45";
when(propertiesDao.findUsersForNotification("NewViolations", "Email", projectUuid))
String projectKey = randomAlphabetic(6);
String otherProjectKey = randomAlphabetic(7);
when(propertiesDao.findUsersForNotification("NewViolations", "Email", projectKey))
.thenReturn(newHashSet(new Subscriber("user1", false), new Subscriber("user3", false), new Subscriber("user3", true)));
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", "uuid_56"))
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", otherProjectKey))
.thenReturn(newHashSet(new Subscriber("user2", false)));
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", projectUuid))
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", projectKey))
.thenReturn(newHashSet(new Subscriber("user3", true)));
when(propertiesDao.findUsersForNotification("NewAlerts", "Twitter", projectUuid))
when(propertiesDao.findUsersForNotification("NewAlerts", "Twitter", projectKey))
.thenReturn(newHashSet(new Subscriber("user4", false)));

when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user1", "user3"), projectUuid, "user"))
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user1", "user3"), projectKey, "user"))
.thenReturn(newHashSet("user1", "user3"));

Multimap<String, NotificationChannel> multiMap = underTest.findSubscribedRecipientsForDispatcher(dispatcher, projectUuid,
SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER);
Multimap<String, NotificationChannel> multiMap = underTest.findSubscribedRecipientsForDispatcher(dispatcher, projectKey,
ALL_MUST_HAVE_ROLE_USER);
assertThat(multiMap.entries()).hasSize(3);

Map<String, Collection<NotificationChannel>> map = multiMap.asMap();
@@ -164,24 +180,25 @@ public class DefaultNotificationManagerTest {

@Test
public void should_apply_distinct_permission_filtering_global_or_project_subscribers() {
String globalPermission = RandomStringUtils.randomAlphanumeric(4);
String projectPermission = RandomStringUtils.randomAlphanumeric(5);
String projectUuid = "uuid_45";
when(propertiesDao.findUsersForNotification("NewViolations", "Email", projectUuid))
String globalPermission = randomAlphanumeric(4);
String projectPermission = randomAlphanumeric(5);
String projectKey = randomAlphabetic(6);
String otherProjectKey = randomAlphabetic(7);
when(propertiesDao.findUsersForNotification("NewViolations", "Email", projectKey))
.thenReturn(newHashSet(new Subscriber("user1", false), new Subscriber("user3", false), new Subscriber("user3", true)));
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", "uuid_56"))
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", otherProjectKey))
.thenReturn(newHashSet(new Subscriber("user2", false)));
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", projectUuid))
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", projectKey))
.thenReturn(newHashSet(new Subscriber("user3", true)));
when(propertiesDao.findUsersForNotification("NewAlerts", "Twitter", projectUuid))
when(propertiesDao.findUsersForNotification("NewAlerts", "Twitter", projectKey))
.thenReturn(newHashSet(new Subscriber("user4", false)));

when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user3", "user4"), projectUuid, globalPermission))
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user3", "user4"), projectKey, globalPermission))
.thenReturn(newHashSet("user3"));
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user1", "user3"), projectUuid, projectPermission))
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user1", "user3"), projectKey, projectPermission))
.thenReturn(newHashSet("user1", "user3"));

Multimap<String, NotificationChannel> multiMap = underTest.findSubscribedRecipientsForDispatcher(dispatcher, projectUuid,
Multimap<String, NotificationChannel> multiMap = underTest.findSubscribedRecipientsForDispatcher(dispatcher, projectKey,
new SubscriberPermissionsOnProject(globalPermission, projectPermission));
assertThat(multiMap.entries()).hasSize(3);

@@ -198,19 +215,19 @@ public class DefaultNotificationManagerTest {

@Test
public void do_not_call_db_for_project_permission_filtering_if_there_is_no_project_subscriber() {
String globalPermission = RandomStringUtils.randomAlphanumeric(4);
String projectPermission = RandomStringUtils.randomAlphanumeric(5);
String projectUuid = "uuid_45";
when(propertiesDao.findUsersForNotification("NewViolations", "Email", projectUuid))
.thenReturn(newHashSet(new Subscriber("user3", true)));
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", projectUuid))
.thenReturn(newHashSet(new Subscriber("user3", true)));
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user3"), projectUuid, globalPermission))
.thenReturn(newHashSet("user3"));
Multimap<String, NotificationChannel> multiMap = underTest.findSubscribedRecipientsForDispatcher(dispatcher, projectUuid,
new SubscriberPermissionsOnProject(globalPermission, projectPermission));
String globalPermission = randomAlphanumeric(4);
String projectPermission = randomAlphanumeric(5);
String projectKey = randomAlphabetic(6);
when(propertiesDao.findUsersForNotification("NewViolations", "Email", projectKey))
.thenReturn(newHashSet(new Subscriber("user3", true)));
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", projectKey))
.thenReturn(newHashSet(new Subscriber("user3", true)));
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user3"), projectKey, globalPermission))
.thenReturn(newHashSet("user3"));
Multimap<String, NotificationChannel> multiMap = underTest.findSubscribedRecipientsForDispatcher(dispatcher, projectKey,
new SubscriberPermissionsOnProject(globalPermission, projectPermission));
assertThat(multiMap.entries()).hasSize(2);

Map<String, Collection<NotificationChannel>> map = multiMap.asMap();
@@ -222,19 +239,19 @@ public class DefaultNotificationManagerTest {

@Test
public void do_not_call_db_for_project_permission_filtering_if_there_is_no_global_subscriber() {
String globalPermission = RandomStringUtils.randomAlphanumeric(4);
String projectPermission = RandomStringUtils.randomAlphanumeric(5);
String projectUuid = "uuid_45";
when(propertiesDao.findUsersForNotification("NewViolations", "Email", projectUuid))
.thenReturn(newHashSet(new Subscriber("user3", false)));
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", projectUuid))
.thenReturn(newHashSet(new Subscriber("user3", false)));
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user3"), projectUuid, projectPermission))
.thenReturn(newHashSet("user3"));
Multimap<String, NotificationChannel> multiMap = underTest.findSubscribedRecipientsForDispatcher(dispatcher, projectUuid,
new SubscriberPermissionsOnProject(globalPermission, projectPermission));
String globalPermission = randomAlphanumeric(4);
String projectPermission = randomAlphanumeric(5);
String projectKey = randomAlphabetic(6);
when(propertiesDao.findUsersForNotification("NewViolations", "Email", projectKey))
.thenReturn(newHashSet(new Subscriber("user3", false)));
when(propertiesDao.findUsersForNotification("NewViolations", "Twitter", projectKey))
.thenReturn(newHashSet(new Subscriber("user3", false)));
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user3"), projectKey, projectPermission))
.thenReturn(newHashSet("user3"));
Multimap<String, NotificationChannel> multiMap = underTest.findSubscribedRecipientsForDispatcher(dispatcher, projectKey,
new SubscriberPermissionsOnProject(globalPermission, projectPermission));
assertThat(multiMap.entries()).hasSize(2);

Map<String, Collection<NotificationChannel>> map = multiMap.asMap();
@@ -243,4 +260,110 @@ public class DefaultNotificationManagerTest {
verify(authorizationDao, times(0)).keepAuthorizedLoginsOnProject(eq(dbSession), anySet(), anyString(), eq(globalPermission));
verify(authorizationDao, times(1)).keepAuthorizedLoginsOnProject(eq(dbSession), anySet(), anyString(), eq(projectPermission));
}

@Test
public void findSubscribedEmailRecipients_fails_with_NPE_if_projectKey_is_null() {
String dispatcherKey = randomAlphabetic(12);

expectedException.expect(NullPointerException.class);
expectedException.expectMessage("projectKey is mandatory");

underTest.findSubscribedEmailRecipients(dispatcherKey, null, ALL_MUST_HAVE_ROLE_USER);
}

@Test
public void findSubscribedEmailRecipients_returns_empty_if_no_email_recipients_in_project_for_dispatcher_key() {
String dispatcherKey = randomAlphabetic(12);
String globalPermission = randomAlphanumeric(4);
String projectPermission = randomAlphanumeric(5);
String projectKey = randomAlphabetic(6);
when(propertiesDao.findEmailSubscribersForNotification(dbSession, dispatcherKey, "EmailNotificationChannel", projectKey))
.thenReturn(Collections.emptySet());

Set<EmailRecipient> emailRecipients = underTest.findSubscribedEmailRecipients(dispatcherKey, projectKey,
new SubscriberPermissionsOnProject(globalPermission, projectPermission));
assertThat(emailRecipients).isEmpty();

verify(authorizationDao, times(0)).keepAuthorizedLoginsOnProject(any(DbSession.class), anySet(), anyString(), anyString());
}

@Test
public void findSubscribedEmailRecipients_applies_distinct_permission_filtering_global_or_project_subscribers() {
String dispatcherKey = randomAlphabetic(12);
String otherDispatcherKey = randomAlphabetic(13);
String globalPermission = randomAlphanumeric(4);
String projectPermission = randomAlphanumeric(5);
String projectKey = randomAlphabetic(6);
String otherProjectKey = randomAlphabetic(7);
when(propertiesDao.findEmailSubscribersForNotification(dbSession, dispatcherKey, "EmailNotificationChannel", projectKey))
.thenReturn(newHashSet(new EmailSubscriberDto("user1", false, "user1@foo"), new EmailSubscriberDto("user3", false, "user3@foo"), new EmailSubscriberDto("user3", true, "user3@foo")));
when(propertiesDao.findEmailSubscribersForNotification(dbSession, dispatcherKey, "EmailNotificationChannel", otherProjectKey))
.thenReturn(newHashSet(new EmailSubscriberDto("user2", false, "user2@foo")));
when(propertiesDao.findEmailSubscribersForNotification(dbSession, otherDispatcherKey, "EmailNotificationChannel", projectKey))
.thenReturn(newHashSet(new EmailSubscriberDto("user4", true, "user4@foo")));

when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user3", "user4"), projectKey, globalPermission))
.thenReturn(newHashSet("user3"));
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, newHashSet("user1", "user3"), projectKey, projectPermission))
.thenReturn(newHashSet("user1", "user3"));

Set<EmailRecipient> emailRecipients = underTest.findSubscribedEmailRecipients(dispatcherKey, projectKey,
new SubscriberPermissionsOnProject(globalPermission, projectPermission));
assertThat(emailRecipients)
.isEqualTo(ImmutableSet.of(new EmailRecipient("user1", "user1@foo"), new EmailRecipient("user3", "user3@foo")));

// code is optimized to perform only 2 SQL requests for all channels
verify(authorizationDao, times(1)).keepAuthorizedLoginsOnProject(eq(dbSession), anySet(), anyString(), eq(globalPermission));
verify(authorizationDao, times(1)).keepAuthorizedLoginsOnProject(eq(dbSession), anySet(), anyString(), eq(projectPermission));
}

@Test
public void findSubscribedEmailRecipients_do_not_call_db_for_project_permission_filtering_if_there_is_no_project_subscriber() {
String dispatcherKey = randomAlphabetic(12);
String globalPermission = randomAlphanumeric(4);
String projectPermission = randomAlphanumeric(5);
String projectKey = randomAlphabetic(6);
Set<EmailSubscriberDto> subscribers = IntStream.range(0, 1 + new Random().nextInt(10))
.mapToObj(i -> new EmailSubscriberDto("user" + i, true, "user" + i + "@sonarsource.com"))
.collect(Collectors.toSet());
when(propertiesDao.findEmailSubscribersForNotification(dbSession, dispatcherKey, "EmailNotificationChannel", projectKey))
.thenReturn(subscribers);
Set<String> logins = subscribers.stream().map(EmailSubscriberDto::getLogin).collect(Collectors.toSet());
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, logins, projectKey, globalPermission))
.thenReturn(logins);

Set<EmailRecipient> emailRecipients = underTest.findSubscribedEmailRecipients(dispatcherKey, projectKey,
new SubscriberPermissionsOnProject(globalPermission, projectPermission));
Set<EmailRecipient> expected = subscribers.stream().map(i -> new EmailRecipient(i.getLogin(), i.getEmail())).collect(Collectors.toSet());
assertThat(emailRecipients)
.isEqualTo(expected);

verify(authorizationDao, times(1)).keepAuthorizedLoginsOnProject(eq(dbSession), anySet(), anyString(), eq(globalPermission));
verify(authorizationDao, times(0)).keepAuthorizedLoginsOnProject(eq(dbSession), anySet(), anyString(), eq(projectPermission));
}

@Test
public void findSubscribedEmailRecipients_does_not_call_DB_for_project_permission_filtering_if_there_is_no_global_subscriber() {
String dispatcherKey = randomAlphabetic(12);
String globalPermission = randomAlphanumeric(4);
String projectPermission = randomAlphanumeric(5);
String projectKey = randomAlphabetic(6);
Set<EmailSubscriberDto> subscribers = IntStream.range(0, 1 + new Random().nextInt(10))
.mapToObj(i -> new EmailSubscriberDto("user" + i, false, "user" + i + "@sonarsource.com"))
.collect(Collectors.toSet());
when(propertiesDao.findEmailSubscribersForNotification(dbSession, dispatcherKey, "EmailNotificationChannel", projectKey))
.thenReturn(subscribers);
Set<String> logins = subscribers.stream().map(EmailSubscriberDto::getLogin).collect(Collectors.toSet());
when(authorizationDao.keepAuthorizedLoginsOnProject(dbSession, logins, projectKey, projectPermission))
.thenReturn(logins);

Set<EmailRecipient> emailRecipients = underTest.findSubscribedEmailRecipients(dispatcherKey, projectKey,
new SubscriberPermissionsOnProject(globalPermission, projectPermission));
Set<EmailRecipient> expected = subscribers.stream().map(i -> new EmailRecipient(i.getLogin(), i.getEmail())).collect(Collectors.toSet());
assertThat(emailRecipients)
.isEqualTo(expected);

verify(authorizationDao, times(0)).keepAuthorizedLoginsOnProject(eq(dbSession), anySet(), anyString(), eq(globalPermission));
verify(authorizationDao, times(1)).keepAuthorizedLoginsOnProject(eq(dbSession), anySet(), anyString(), eq(projectPermission));
}
}

+ 92
- 0
server/sonar-server-common/src/test/java/org/sonar/server/notification/EmailRecipientTest.java View File

@@ -0,0 +1,92 @@
/*
* 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 org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.server.notification.NotificationManager.EmailRecipient;

import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;

public class EmailRecipientTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void constructor_fails_with_NPE_if_login_is_null() {
String email = randomAlphabetic(12);

expectedException.expect(NullPointerException.class);
expectedException.expectMessage("login can't be null");

new EmailRecipient(null, email);
}

@Test
public void constructor_fails_with_NPE_if_email_is_null() {
String login = randomAlphabetic(12);

expectedException.expect(NullPointerException.class);
expectedException.expectMessage("email can't be null");

new EmailRecipient(login, null);
}

@Test
public void equals_is_based_on_login_and_email() {
String login = randomAlphabetic(11);
String email = randomAlphabetic(12);
EmailRecipient underTest = new EmailRecipient(login, email);

assertThat(underTest)
.isEqualTo(new EmailRecipient(login, email))
.isNotEqualTo(null)
.isNotEqualTo(new Object())
.isNotEqualTo(new EmailRecipient(email, login))
.isNotEqualTo(new EmailRecipient(randomAlphabetic(5), email))
.isNotEqualTo(new EmailRecipient(login, randomAlphabetic(5)))
.isNotEqualTo(new EmailRecipient(randomAlphabetic(5), randomAlphabetic(6)));
}

@Test
public void hashcode_is_based_on_login_and_email() {
String login = randomAlphabetic(11);
String email = randomAlphabetic(12);
EmailRecipient underTest = new EmailRecipient(login, email);

assertThat(underTest.hashCode())
.isEqualTo(new EmailRecipient(login, email).hashCode())
.isNotEqualTo(new Object().hashCode())
.isNotEqualTo(new EmailRecipient(email, login).hashCode())
.isNotEqualTo(new EmailRecipient(randomAlphabetic(5), email).hashCode())
.isNotEqualTo(new EmailRecipient(login, randomAlphabetic(5)).hashCode())
.isNotEqualTo(new EmailRecipient(randomAlphabetic(5), randomAlphabetic(6)).hashCode());
}

@Test
public void verify_to_String() {
String login = randomAlphabetic(11);
String email = randomAlphabetic(12);

assertThat(new EmailRecipient(login, email).toString()).isEqualTo("EmailRecipient{'" + login + "':'" + email + "'}");
}
}

+ 202
- 0
server/sonar-server-common/src/test/java/org/sonar/server/notification/NotificationServiceTest.java View File

@@ -0,0 +1,202 @@
/*
* 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 java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.notifications.Notification;
import org.sonar.db.DbClient;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

public class NotificationServiceTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();

private final DbClient dbClient = mock(DbClient.class);

@Test
public void deliverEmails_fails_with_IAE_if_type_of_collection_is_Notification() {
NotificationHandler handler = mock(NotificationHandler1.class);
List<Notification> notifications = IntStream.range(0, 1 + new Random().nextInt(20))
.mapToObj(i -> new Notification("i"))
.collect(Collectors.toList());
NotificationService underTest = new NotificationService(dbClient, new NotificationHandler[] {handler});

expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Type of notification objects must be a subtype of Notification");

underTest.deliverEmails(notifications);
}

@Test
public void deliverEmails_collection_has_no_effect_if_no_handler_nor_dispatcher() {
List<Notification> notifications = IntStream.range(0, 1 + new Random().nextInt(20))
.mapToObj(i -> mock(Notification.class))
.collect(Collectors.toList());
NotificationService underTest = new NotificationService(dbClient);

assertThat(underTest.deliverEmails(notifications)).isZero();
verifyZeroInteractions(dbClient);
}

@Test
public void deliverEmails_collection_has_no_effect_if_no_handler() {
NotificationDispatcher dispatcher = mock(NotificationDispatcher.class);
List<Notification> notifications = IntStream.range(0, new Random().nextInt(20))
.mapToObj(i -> mock(Notification.class))
.collect(Collectors.toList());
NotificationService underTest = new NotificationService(dbClient, new NotificationDispatcher[] {dispatcher});

assertThat(underTest.deliverEmails(notifications)).isZero();
verifyZeroInteractions(dispatcher);
verifyZeroInteractions(dbClient);
}

@Test
public void deliverEmails_collection_returns_0_if_collection_is_empty() {
NotificationHandler1 handler1 = mock(NotificationHandler1.class);
NotificationHandler2 handler2 = mock(NotificationHandler2.class);
NotificationService underTest = new NotificationService(dbClient,
new NotificationHandler[] {handler1, handler2});

assertThat(underTest.deliverEmails(Collections.emptyList())).isZero();
verifyZeroInteractions(handler1, handler2);
}

@Test
public void deliverEmails_collection_returns_0_if_no_handler_for_the_notification_class() {
NotificationHandler1 handler1 = mock(NotificationHandler1.class);
NotificationHandler2 handler2 = mock(NotificationHandler2.class);
List<Notification1> notification1s = IntStream.range(0, 1 + new Random().nextInt(20))
.mapToObj(i -> new Notification1())
.collect(Collectors.toList());
List<Notification2> notification2s = IntStream.range(0, 1 + new Random().nextInt(20))
.mapToObj(i -> new Notification2())
.collect(Collectors.toList());
NotificationService noHandler = new NotificationService(dbClient);
NotificationService onlyHandler1 = new NotificationService(dbClient, new NotificationHandler[] {handler1});
NotificationService onlyHandler2 = new NotificationService(dbClient, new NotificationHandler[] {handler2});

assertThat(noHandler.deliverEmails(notification1s)).isZero();
assertThat(noHandler.deliverEmails(notification2s)).isZero();
assertThat(onlyHandler1.deliverEmails(notification2s)).isZero();
assertThat(onlyHandler2.deliverEmails(notification1s)).isZero();
verify(handler1, times(0)).deliver(anyCollection());
verify(handler2, times(0)).deliver(anyCollection());
}

@Test
public void deliverEmails_collection_calls_deliver_method_of_handler_for_notification_class_and_returns_its_output() {
Random random = new Random();
NotificationHandler1 handler1 = mock(NotificationHandler1.class);
NotificationHandler2 handler2 = mock(NotificationHandler2.class);
List<Notification1> notification1s = IntStream.range(0, 1 + random.nextInt(20))
.mapToObj(i -> new Notification1())
.collect(Collectors.toList());
List<Notification2> notification2s = IntStream.range(0, 1 + random.nextInt(20))
.mapToObj(i -> new Notification2())
.collect(Collectors.toList());
NotificationService onlyHandler1 = new NotificationService(dbClient, new NotificationHandler[] {handler1});
NotificationService onlyHandler2 = new NotificationService(dbClient, new NotificationHandler[] {handler2});
NotificationService bothHandlers = new NotificationService(dbClient, new NotificationHandler[] {handler1, handler2});

int expected = randomDeliveredCount(notification1s);
when(handler1.deliver(notification1s)).thenReturn(expected);
assertThat(onlyHandler1.deliverEmails(notification1s)).isEqualTo(expected);
verify(handler1).deliver(notification1s);
verify(handler2, times(0)).deliver(anyCollection());

reset(handler1, handler2);
expected = randomDeliveredCount(notification2s);
when(handler2.deliver(notification2s)).thenReturn(expected);
assertThat(onlyHandler2.deliverEmails(notification2s)).isEqualTo(expected);
verify(handler2).deliver(notification2s);
verify(handler1, times(0)).deliver(anyCollection());

reset(handler1, handler2);
expected = randomDeliveredCount(notification1s);
when(handler1.deliver(notification1s)).thenReturn(expected);
assertThat(bothHandlers.deliverEmails(notification1s)).isEqualTo(expected);
verify(handler1).deliver(notification1s);
verify(handler2, times(0)).deliver(anyCollection());

reset(handler1, handler2);
expected = randomDeliveredCount(notification2s);
when(handler2.deliver(notification2s)).thenReturn(expected);
assertThat(bothHandlers.deliverEmails(notification2s)).isEqualTo(expected);
verify(handler2).deliver(notification2s);
verify(handler1, times(0)).deliver(anyCollection());
}

private static final class Notification1 extends Notification {

public Notification1() {
super("1");
}
}

private static abstract class NotificationHandler1 implements NotificationHandler<Notification1> {

// final to prevent mock to override implementation
@Override
public final Class<Notification1> getNotificationClass() {
return Notification1.class;
}

}

private static final class Notification2 extends Notification {

public Notification2() {
super("2");
}
}

private static abstract class NotificationHandler2 implements NotificationHandler<Notification2> {

// final to prevent mock to override implementation
@Override
public final Class<Notification2> getNotificationClass() {
return Notification2.class;
}
}

private static <T extends Notification> int randomDeliveredCount(List<T> notifications) {
int size = notifications.size();
if (size == 1) {
return size;
}
return 1 + new Random().nextInt(size - 1);
}
}

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/notification/ws/AddAction.java View File

@@ -33,7 +33,7 @@ import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.issue.notification.MyNewIssuesNotificationDispatcher;
import org.sonar.server.issue.notification.MyNewIssuesNotificationHandler;
import org.sonar.server.notification.NotificationCenter;
import org.sonar.server.notification.NotificationUpdater;
import org.sonar.server.notification.email.EmailNotificationChannel;
@@ -100,7 +100,7 @@ public class AddAction implements NotificationsWsAction {
String.join(", ", dispatchers.getGlobalDispatchers()),
String.join(", ", dispatchers.getProjectDispatchers()))
.setRequired(true)
.setExampleValue(MyNewIssuesNotificationDispatcher.KEY);
.setExampleValue(MyNewIssuesNotificationHandler.KEY);

action.createParam(PARAM_LOGIN)
.setDescription("User login")

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/notification/ws/RemoveAction.java View File

@@ -33,7 +33,7 @@ import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.issue.notification.MyNewIssuesNotificationDispatcher;
import org.sonar.server.issue.notification.MyNewIssuesNotificationHandler;
import org.sonar.server.notification.NotificationCenter;
import org.sonar.server.notification.NotificationUpdater;
import org.sonar.server.notification.email.EmailNotificationChannel;
@@ -100,7 +100,7 @@ public class RemoveAction implements NotificationsWsAction {
dispatchers.getGlobalDispatchers().stream().sorted().collect(Collectors.joining(", ")),
dispatchers.getProjectDispatchers().stream().sorted().collect(Collectors.joining(", ")))
.setRequired(true)
.setExampleValue(MyNewIssuesNotificationDispatcher.KEY);
.setExampleValue(MyNewIssuesNotificationHandler.KEY);

action.createParam(PARAM_LOGIN)
.setDescription("User login")

+ 3
- 3
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java View File

@@ -84,7 +84,7 @@ import org.sonar.server.issue.notification.ChangesOnMyIssueNotificationDispatche
import org.sonar.server.issue.notification.DoNotFixNotificationDispatcher;
import org.sonar.server.issue.notification.IssueChangesEmailTemplate;
import org.sonar.server.issue.notification.MyNewIssuesEmailTemplate;
import org.sonar.server.issue.notification.MyNewIssuesNotificationDispatcher;
import org.sonar.server.issue.notification.MyNewIssuesNotificationHandler;
import org.sonar.server.issue.notification.NewIssuesEmailTemplate;
import org.sonar.server.issue.notification.NewIssuesNotificationDispatcher;
import org.sonar.server.issue.notification.NewIssuesNotificationFactory;
@@ -414,8 +414,8 @@ public class PlatformLevel4 extends PlatformLevel {
ChangesOnMyIssueNotificationDispatcher.newMetadata(),
NewIssuesNotificationDispatcher.class,
NewIssuesNotificationDispatcher.newMetadata(),
MyNewIssuesNotificationDispatcher.class,
MyNewIssuesNotificationDispatcher.newMetadata(),
MyNewIssuesNotificationHandler.class,
MyNewIssuesNotificationHandler.newMetadata(),
DoNotFixNotificationDispatcher.class,
DoNotFixNotificationDispatcher.newMetadata(),
NewIssuesNotificationFactory.class,

+ 22
- 0
server/sonar-server/src/test/java/org/sonar/server/notification/email/EmailNotificationChannelTest.java View File

@@ -33,6 +33,7 @@ import org.subethamail.wiser.Wiser;
import org.subethamail.wiser.WiserMessage;

import static junit.framework.Assert.fail;
import static org.apache.commons.lang.RandomStringUtils.random;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -60,6 +61,27 @@ public class EmailNotificationChannelTest {
smtpServer.stop();
}

@Test
public void isActivated_returns_true_if_smpt_host_is_not_empty() {
when(configuration.getSmtpHost()).thenReturn(random(5));

assertThat(underTest.isActivated()).isTrue();
}

@Test
public void isActivated_returns_false_if_smpt_host_is_null() {
when(configuration.getSmtpHost()).thenReturn(null);

assertThat(underTest.isActivated()).isFalse();
}

@Test
public void isActivated_returns_false_if_smpt_host_is_empty() {
when(configuration.getSmtpHost()).thenReturn("");

assertThat(underTest.isActivated()).isFalse();
}

@Test
public void shouldSendTestEmail() throws Exception {
configure();

+ 6
- 6
server/sonar-server/src/test/java/org/sonar/server/notification/ws/DispatchersImplTest.java View File

@@ -24,7 +24,7 @@ import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.notifications.NotificationChannel;
import org.sonar.server.event.NewAlerts;
import org.sonar.server.issue.notification.DoNotFixNotificationDispatcher;
import org.sonar.server.issue.notification.MyNewIssuesNotificationDispatcher;
import org.sonar.server.issue.notification.MyNewIssuesNotificationHandler;
import org.sonar.server.issue.notification.NewIssuesNotificationDispatcher;
import org.sonar.server.notification.NotificationCenter;
import org.sonar.server.notification.NotificationDispatcherMetadata;
@@ -37,7 +37,7 @@ public class DispatchersImplTest {

private NotificationCenter notificationCenter = new NotificationCenter(
new NotificationDispatcherMetadata[] {
NotificationDispatcherMetadata.create(MyNewIssuesNotificationDispatcher.KEY)
NotificationDispatcherMetadata.create(MyNewIssuesNotificationHandler.KEY)
.setProperty(GLOBAL_NOTIFICATION, "true")
.setProperty(PER_PROJECT_NOTIFICATION, "true"),
NotificationDispatcherMetadata.create(NewIssuesNotificationDispatcher.KEY)
@@ -60,7 +60,7 @@ public class DispatchersImplTest {
underTest.start();

assertThat(underTest.getGlobalDispatchers()).containsExactly(
NewAlerts.KEY, DoNotFixNotificationDispatcher.KEY, NewIssuesNotificationDispatcher.KEY, MyNewIssuesNotificationDispatcher.KEY);
NewAlerts.KEY, DoNotFixNotificationDispatcher.KEY, NewIssuesNotificationDispatcher.KEY, MyNewIssuesNotificationHandler.KEY);
}

@Test
@@ -69,7 +69,7 @@ public class DispatchersImplTest {

underTest.start();

assertThat(underTest.getGlobalDispatchers()).containsOnly(MyNewIssuesNotificationDispatcher.KEY);
assertThat(underTest.getGlobalDispatchers()).containsOnly(MyNewIssuesNotificationHandler.KEY);
}

@Test
@@ -77,7 +77,7 @@ public class DispatchersImplTest {
underTest.start();

assertThat(underTest.getProjectDispatchers()).containsExactly(
NewAlerts.KEY, DoNotFixNotificationDispatcher.KEY, MyNewIssuesNotificationDispatcher.KEY);
NewAlerts.KEY, DoNotFixNotificationDispatcher.KEY, MyNewIssuesNotificationHandler.KEY);
}

@Test
@@ -87,6 +87,6 @@ public class DispatchersImplTest {
underTest.start();

assertThat(underTest.getProjectDispatchers()).containsOnly(
MyNewIssuesNotificationDispatcher.KEY, NewAlerts.KEY, DoNotFixNotificationDispatcher.KEY);
MyNewIssuesNotificationHandler.KEY, NewAlerts.KEY, DoNotFixNotificationDispatcher.KEY);
}
}

Loading…
Cancel
Save