@@ -99,7 +99,7 @@ import org.sonar.ce.task.projectanalysis.measure.MeasureComputersVisitor; | |||
import org.sonar.ce.task.projectanalysis.measure.MeasureRepositoryImpl; | |||
import org.sonar.ce.task.projectanalysis.measure.MeasureToMeasureDto; | |||
import org.sonar.ce.task.projectanalysis.metric.MetricModule; | |||
import org.sonar.ce.task.projectanalysis.notification.NewIssuesNotificationFactory; | |||
import org.sonar.ce.task.projectanalysis.notification.NotificationFactory; | |||
import org.sonar.ce.task.projectanalysis.organization.DefaultOrganizationLoader; | |||
import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl; | |||
import org.sonar.ce.task.projectanalysis.qualitygate.EvaluationResultTextConverterImpl; | |||
@@ -306,7 +306,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop | |||
WebhookPostTask.class, | |||
// notifications | |||
NewIssuesNotificationFactory.class); | |||
NotificationFactory.class); | |||
} | |||
} |
@@ -22,35 +22,56 @@ package org.sonar.ce.task.projectanalysis.notification; | |||
import com.google.common.collect.ImmutableMap; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.utils.Durations; | |||
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder; | |||
import org.sonar.ce.task.projectanalysis.analysis.Branch; | |||
import org.sonar.ce.task.projectanalysis.component.Component; | |||
import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler; | |||
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; | |||
import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter; | |||
import org.sonar.ce.task.projectanalysis.issue.RuleRepository; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.notification.IssuesChangesNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.issue.notification.MyNewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesNotification.DetailsSupplier; | |||
import org.sonar.server.issue.notification.NewIssuesNotification.RuleDefinition; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.PRE_ORDER; | |||
import static org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit.FILE; | |||
import static org.sonar.db.component.BranchType.PULL_REQUEST; | |||
@ComputeEngineSide | |||
public class NewIssuesNotificationFactory { | |||
public class NotificationFactory { | |||
private final TreeRootHolder treeRootHolder; | |||
private final AnalysisMetadataHolder analysisMetadataHolder; | |||
private final RuleRepository ruleRepository; | |||
private final Durations durations; | |||
private final IssuesChangesNotificationSerializer issuesChangesSerializer; | |||
private Map<String, Component> componentsByUuid; | |||
public NewIssuesNotificationFactory(TreeRootHolder treeRootHolder, RuleRepository ruleRepository, Durations durations) { | |||
public NotificationFactory(TreeRootHolder treeRootHolder, AnalysisMetadataHolder analysisMetadataHolder, | |||
RuleRepository ruleRepository, Durations durations, IssuesChangesNotificationSerializer issuesChangesSerializer) { | |||
this.treeRootHolder = treeRootHolder; | |||
this.analysisMetadataHolder = analysisMetadataHolder; | |||
this.ruleRepository = ruleRepository; | |||
this.durations = durations; | |||
this.issuesChangesSerializer = issuesChangesSerializer; | |||
} | |||
public MyNewIssuesNotification newMyNewIssuesNotification(Map<String, UserDto> assigneesByUuid) { | |||
@@ -63,6 +84,49 @@ public class NewIssuesNotificationFactory { | |||
return new NewIssuesNotification(durations, new DetailsSupplierImpl(assigneesByUuid)); | |||
} | |||
public IssuesChangesNotification newIssuesChangesNotification(Set<DefaultIssue> issues, Map<String, UserDto> assigneesByUuid) { | |||
AnalysisChange change = new AnalysisChange(analysisMetadataHolder.getAnalysisDate()); | |||
Set<ChangedIssue> changedIssues = issues.stream() | |||
.map(issue -> new ChangedIssue.Builder(issue.key()) | |||
.setAssignee(getAssignee(issue.assignee(), assigneesByUuid)) | |||
.setNewResolution(issue.resolution()) | |||
.setNewStatus(issue.status()) | |||
.setRule(getRuleByRuleKey(issue.ruleKey())) | |||
.setProject(getProject()) | |||
.build()) | |||
.collect(MoreCollectors.toSet(issues.size())); | |||
return issuesChangesSerializer.serialize(new IssuesChangesNotificationBuilder(changedIssues, change)); | |||
} | |||
@CheckForNull | |||
public User getAssignee(@Nullable String assigneeUuid, Map<String, UserDto> assigneesByUuid) { | |||
if (assigneeUuid == null) { | |||
return null; | |||
} | |||
UserDto dto = assigneesByUuid.get(assigneeUuid); | |||
checkState(dto != null, "Can not find DTO for assignee uuid %s", assigneeUuid); | |||
return new User(dto.getUuid(), dto.getLogin(), dto.getName()); | |||
} | |||
private IssuesChangesNotificationBuilder.Rule getRuleByRuleKey(RuleKey ruleKey) { | |||
return ruleRepository.findByKey(ruleKey) | |||
.map(t -> new IssuesChangesNotificationBuilder.Rule(ruleKey, t.getName())) | |||
.orElseThrow(() -> new IllegalStateException("Can not find rule " + ruleKey + " in RuleRepository")); | |||
} | |||
private Project getProject() { | |||
Component project = treeRootHolder.getRoot(); | |||
Branch branch = analysisMetadataHolder.getBranch(); | |||
Project.Builder builder = new Project.Builder(project.getUuid()) | |||
.setKey(project.getKey()) | |||
.setProjectName(project.getName()); | |||
if (!branch.isLegacyFeature() && branch.getType() != PULL_REQUEST && !branch.isMain()) { | |||
builder.setBranchName(branch.getName()); | |||
} | |||
return builder.build(); | |||
} | |||
private static void verifyAssigneesByUuid(Map<String, UserDto> assigneesByUuid) { | |||
requireNonNull(assigneesByUuid, "assigneesByUuid can't be null"); | |||
} |
@@ -21,6 +21,7 @@ package org.sonar.ce.task.projectanalysis.notification; | |||
import java.io.UnsupportedEncodingException; | |||
import java.net.URLEncoder; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.server.issue.notification.EmailMessage; | |||
@@ -41,6 +42,7 @@ public class ReportAnalysisFailureNotificationEmailTemplate implements EmailTemp | |||
} | |||
@Override | |||
@CheckForNull | |||
public EmailMessage format(Notification notification) { | |||
if (!(notification instanceof ReportAnalysisFailureNotification)) { | |||
return null; | |||
@@ -53,7 +55,7 @@ public class ReportAnalysisFailureNotificationEmailTemplate implements EmailTemp | |||
return new EmailMessage() | |||
.setMessageId(notification.getType() + "/" + projectUuid) | |||
.setSubject(subject(projectFullName)) | |||
.setMessage(message(projectFullName, taskFailureNotification)); | |||
.setPlainTextMessage(message(projectFullName, taskFailureNotification)); | |||
} | |||
private static String computeProjectFullName(ReportAnalysisFailureNotificationBuilder.Project project) { |
@@ -19,19 +19,17 @@ | |||
*/ | |||
package org.sonar.ce.task.projectanalysis.step; | |||
import com.google.common.collect.ImmutableMap; | |||
import com.google.common.collect.ImmutableSet; | |||
import java.time.Instant; | |||
import java.time.temporal.ChronoUnit; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.Date; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Predicate; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.notifications.Notification; | |||
@@ -40,22 +38,17 @@ import org.sonar.api.utils.Duration; | |||
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder; | |||
import org.sonar.ce.task.projectanalysis.analysis.Branch; | |||
import org.sonar.ce.task.projectanalysis.component.Component; | |||
import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit; | |||
import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler; | |||
import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; | |||
import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter; | |||
import org.sonar.ce.task.projectanalysis.issue.IssueCache; | |||
import org.sonar.ce.task.projectanalysis.issue.RuleRepository; | |||
import org.sonar.ce.task.projectanalysis.notification.NewIssuesNotificationFactory; | |||
import org.sonar.ce.task.projectanalysis.notification.NotificationFactory; | |||
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.component.BranchType; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.notification.IssueChangeNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotification; | |||
import org.sonar.server.issue.notification.MyNewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesStatistics; | |||
@@ -64,9 +57,8 @@ import org.sonar.server.notification.NotificationService; | |||
import static java.util.Collections.singleton; | |||
import static java.util.stream.Collectors.toList; | |||
import static java.util.stream.Collectors.toMap; | |||
import static java.util.stream.Collectors.toSet; | |||
import static java.util.stream.StreamSupport.stream; | |||
import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.db.component.BranchType.PULL_REQUEST; | |||
import static org.sonar.db.component.BranchType.SHORT; | |||
@@ -79,27 +71,25 @@ public class SendIssueNotificationsStep implements ComputationStep { | |||
/** | |||
* Types of the notifications sent by this step | |||
*/ | |||
static final Set<Class<? extends Notification>> NOTIF_TYPES = ImmutableSet.of(NewIssuesNotification.class, MyNewIssuesNotification.class, IssueChangeNotification.class); | |||
static final Set<Class<? extends Notification>> NOTIF_TYPES = ImmutableSet.of(NewIssuesNotification.class, MyNewIssuesNotification.class, IssuesChangesNotification.class); | |||
private final IssueCache issueCache; | |||
private final RuleRepository rules; | |||
private final TreeRootHolder treeRootHolder; | |||
private final NotificationService service; | |||
private final AnalysisMetadataHolder analysisMetadataHolder; | |||
private final NewIssuesNotificationFactory newIssuesNotificationFactory; | |||
private final NotificationFactory notificationFactory; | |||
private final DbClient dbClient; | |||
private Map<String, Component> componentsByDbKey; | |||
public SendIssueNotificationsStep(IssueCache issueCache, RuleRepository rules, TreeRootHolder treeRootHolder, | |||
public SendIssueNotificationsStep(IssueCache issueCache, TreeRootHolder treeRootHolder, | |||
NotificationService service, AnalysisMetadataHolder analysisMetadataHolder, | |||
NewIssuesNotificationFactory newIssuesNotificationFactory, DbClient dbClient) { | |||
NotificationFactory notificationFactory, DbClient dbClient) { | |||
this.issueCache = issueCache; | |||
this.rules = rules; | |||
this.treeRootHolder = treeRootHolder; | |||
this.service = service; | |||
this.analysisMetadataHolder = analysisMetadataHolder; | |||
this.newIssuesNotificationFactory = newIssuesNotificationFactory; | |||
this.notificationFactory = notificationFactory; | |||
this.dbClient = dbClient; | |||
} | |||
@@ -125,12 +115,12 @@ public class SendIssueNotificationsStep implements ComputationStep { | |||
Map<String, UserDto> assigneesByUuid; | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
Iterable<DefaultIssue> iterable = issueCache::traverse; | |||
Set<String> assigneeUuids = stream(iterable.spliterator(), false).map(DefaultIssue::assignee).filter(Objects::nonNull).collect(toSet()); | |||
Set<String> assigneeUuids = stream(iterable.spliterator(), false).map(DefaultIssue::assignee).filter(Objects::nonNull).collect(Collectors.toSet()); | |||
assigneesByUuid = dbClient.userDao().selectByUuids(dbSession, assigneeUuids).stream().collect(toMap(UserDto::getUuid, dto -> dto)); | |||
} | |||
try (CloseableIterator<DefaultIssue> issues = issueCache.traverse()) { | |||
processIssues(newIssuesStats, issues, project, assigneesByUuid, notificationStatistics); | |||
processIssues(newIssuesStats, issues, assigneesByUuid, notificationStatistics); | |||
} | |||
if (newIssuesStats.hasIssuesOnLeak()) { | |||
sendNewIssuesNotification(newIssuesStats, project, assigneesByUuid, analysisDate, notificationStatistics); | |||
@@ -148,56 +138,45 @@ public class SendIssueNotificationsStep implements ComputationStep { | |||
return Date.from(instant).getTime(); | |||
} | |||
private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues, Component project, Map<String, UserDto> usersDtoByUuids, | |||
NotificationStatistics notificationStatistics) { | |||
private void processIssues(NewIssuesStatistics newIssuesStats, CloseableIterator<DefaultIssue> issues, | |||
Map<String, UserDto> assigneesByUuid, NotificationStatistics notificationStatistics) { | |||
int batchSize = 1000; | |||
List<DefaultIssue> loadedIssues = new ArrayList<>(batchSize); | |||
Set<DefaultIssue> changedIssuesToNotify = new HashSet<>(batchSize); | |||
while (issues.hasNext()) { | |||
DefaultIssue issue = issues.next(); | |||
if (issue.type() != RuleType.SECURITY_HOTSPOT) { | |||
if (issue.isNew() && issue.resolution() == null) { | |||
newIssuesStats.add(issue); | |||
} else if (issue.isChanged() && issue.mustSendNotifications()) { | |||
loadedIssues.add(issue); | |||
changedIssuesToNotify.add(issue); | |||
} | |||
} | |||
if (loadedIssues.size() >= batchSize) { | |||
sendIssueChangeNotification(loadedIssues, project, usersDtoByUuids, notificationStatistics); | |||
loadedIssues.clear(); | |||
if (changedIssuesToNotify.size() >= batchSize) { | |||
sendIssuesChangesNotification(changedIssuesToNotify, assigneesByUuid, notificationStatistics); | |||
changedIssuesToNotify.clear(); | |||
} | |||
} | |||
if (!loadedIssues.isEmpty()) { | |||
sendIssueChangeNotification(loadedIssues, project, usersDtoByUuids, notificationStatistics); | |||
if (!changedIssuesToNotify.isEmpty()) { | |||
sendIssuesChangesNotification(changedIssuesToNotify, assigneesByUuid, notificationStatistics); | |||
} | |||
} | |||
private void sendIssueChangeNotification(Collection<DefaultIssue> issues, Component project, Map<String, UserDto> usersDtoByUuids, | |||
NotificationStatistics notificationStatistics) { | |||
Set<IssueChangeNotification> notifications = issues.stream() | |||
.map(issue -> { | |||
IssueChangeNotification notification = new IssueChangeNotification(); | |||
notification.setRuleName(rules.getByKey(issue.ruleKey()).getName()); | |||
notification.setIssue(issue); | |||
notification.setAssignee(usersDtoByUuids.get(issue.assignee())); | |||
notification.setProject(project.getKey(), project.getName(), getBranchName(), getPullRequest()); | |||
getComponentKey(issue).ifPresent(c -> notification.setComponent(c.getKey(), c.getName())); | |||
return notification; | |||
}) | |||
.collect(MoreCollectors.toSet(issues.size())); | |||
private void sendIssuesChangesNotification(Set<DefaultIssue> issues, Map<String, UserDto> assigneesByUuid, NotificationStatistics notificationStatistics) { | |||
IssuesChangesNotification notification = notificationFactory.newIssuesChangesNotification(issues, assigneesByUuid); | |||
notificationStatistics.issueChangesDeliveries += service.deliverEmails(notifications); | |||
notificationStatistics.issueChangesDeliveries += service.deliverEmails(singleton(notification)); | |||
notificationStatistics.issueChanges++; | |||
// compatibility with old API | |||
notifications.forEach(notification -> notificationStatistics.issueChangesDeliveries += service.deliver(notification)); | |||
notificationStatistics.issueChangesDeliveries += service.deliver(notification); | |||
} | |||
private void sendNewIssuesNotification(NewIssuesStatistics statistics, Component project, Map<String, UserDto> assigneesByUuid, | |||
long analysisDate, NotificationStatistics notificationStatistics) { | |||
NewIssuesStatistics.Stats globalStatistics = statistics.globalStatistics(); | |||
NewIssuesNotification notification = newIssuesNotificationFactory | |||
NewIssuesNotification notification = notificationFactory | |||
.newNewIssuesNotification(assigneesByUuid) | |||
.setProject(project.getKey(), project.getName(), getBranchName(), getPullRequest()) | |||
.setProjectVersion(project.getProjectAttributes().getProjectVersion()) | |||
@@ -220,7 +199,7 @@ public class SendIssueNotificationsStep implements ComputationStep { | |||
.map(e -> { | |||
String assigneeUuid = e.getKey(); | |||
NewIssuesStatistics.Stats assigneeStatistics = e.getValue(); | |||
MyNewIssuesNotification myNewIssuesNotification = newIssuesNotificationFactory | |||
MyNewIssuesNotification myNewIssuesNotification = notificationFactory | |||
.newMyNewIssuesNotification(assigneesByUuid) | |||
.setAssignee(userDtoByUuid.get(assigneeUuid)); | |||
myNewIssuesNotification | |||
@@ -232,7 +211,7 @@ public class SendIssueNotificationsStep implements ComputationStep { | |||
return myNewIssuesNotification; | |||
}) | |||
.collect(MoreCollectors.toSet(statistics.getAssigneesStatistics().size())); | |||
.collect(toSet(statistics.getAssigneesStatistics().size())); | |||
notificationStatistics.myNewIssuesDeliveries += service.deliverEmails(myNewIssuesNotifications); | |||
notificationStatistics.myNewIssues += myNewIssuesNotifications.size(); | |||
@@ -251,21 +230,6 @@ public class SendIssueNotificationsStep implements ComputationStep { | |||
} | |||
} | |||
private Optional<Component> getComponentKey(DefaultIssue issue) { | |||
if (componentsByDbKey == null) { | |||
final ImmutableMap.Builder<String, Component> builder = ImmutableMap.builder(); | |||
new DepthTraversalTypeAwareCrawler( | |||
new TypeAwareVisitorAdapter(CrawlerDepthLimit.LEAVES, POST_ORDER) { | |||
@Override | |||
public void visitAny(Component component) { | |||
builder.put(component.getDbKey(), component); | |||
} | |||
}).visit(this.treeRootHolder.getRoot()); | |||
this.componentsByDbKey = builder.build(); | |||
} | |||
return Optional.ofNullable(componentsByDbKey.get(issue.componentKey())); | |||
} | |||
@Override | |||
public String getDescription() { | |||
return "Send issue notifications"; |
@@ -46,6 +46,7 @@ public class DumbRule implements Rule { | |||
public DumbRule(RuleKey key) { | |||
this.key = key; | |||
this.id = key.hashCode(); | |||
this.name = "name_" + key; | |||
} | |||
@Override |
@@ -20,7 +20,13 @@ | |||
package org.sonar.ce.task.projectanalysis.notification; | |||
import com.google.common.collect.ImmutableMap; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.lang.reflect.Field; | |||
import java.util.Collections; | |||
import java.util.Map; | |||
import java.util.Random; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
@@ -29,36 +35,58 @@ import java.util.stream.Stream; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.junit.runner.RunWith; | |||
import org.mockito.ArgumentCaptor; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.utils.Durations; | |||
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule; | |||
import org.sonar.ce.task.projectanalysis.analysis.Branch; | |||
import org.sonar.ce.task.projectanalysis.component.ReportComponent; | |||
import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule; | |||
import org.sonar.ce.task.projectanalysis.issue.DumbRule; | |||
import org.sonar.ce.task.projectanalysis.issue.RuleRepositoryRule; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.db.component.BranchType; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.db.user.UserTesting; | |||
import org.sonar.server.issue.notification.IssuesChangesNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.issue.notification.MyNewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesNotification.DetailsSupplier; | |||
import org.sonar.server.issue.notification.NewIssuesNotification.RuleDefinition; | |||
import static java.util.Collections.emptyMap; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.verifyNoMoreInteractions; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.api.issue.Issue.STATUS_OPEN; | |||
import static org.sonar.ce.task.projectanalysis.component.Component.Type.DIRECTORY; | |||
import static org.sonar.ce.task.projectanalysis.component.Component.Type.FILE; | |||
import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
public class NewIssuesNotificationFactoryTest { | |||
@RunWith(DataProviderRunner.class) | |||
public class NotificationFactoryTest { | |||
@Rule | |||
public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule(); | |||
@Rule | |||
public RuleRepositoryRule ruleRepository = new RuleRepositoryRule(); | |||
@Rule | |||
public AnalysisMetadataHolderRule analysisMetadata = new AnalysisMetadataHolderRule(); | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private Durations durations = new Durations(); | |||
private NewIssuesNotificationFactory underTest = new NewIssuesNotificationFactory(treeRootHolder, ruleRepository, durations); | |||
private IssuesChangesNotificationSerializer issuesChangesSerializer = mock(IssuesChangesNotificationSerializer.class); | |||
private NotificationFactory underTest = new NotificationFactory(treeRootHolder, analysisMetadata, ruleRepository, durations, issuesChangesSerializer); | |||
@Test | |||
public void newMyNewIssuesNotification_throws_NPE_if_assigneesByUuid_is_null() { | |||
@@ -392,6 +420,375 @@ public class NewIssuesNotificationFactoryTest { | |||
.isEmpty(); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_fails_with_ISE_if_analysis_date_has_not_been_set() { | |||
Set<DefaultIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(i -> new DefaultIssue()) | |||
.collect(Collectors.toSet()); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Analysis date has not been set"); | |||
underTest.newIssuesChangesNotification(issues, assigneesByUuid); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_fails_with_IAE_if_issues_is_empty() { | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("issues can't be empty"); | |||
underTest.newIssuesChangesNotification(Collections.emptySet(), assigneesByUuid); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_fails_with_NPE_if_issue_has_no_rule() { | |||
DefaultIssue issue = new DefaultIssue(); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
expectedException.expect(NullPointerException.class); | |||
underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_fails_with_ISE_if_rule_of_issue_does_not_exist_in_repository() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Can not find rule " + ruleKey + " in RuleRepository"); | |||
underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_fails_with_ISE_if_treeRootHolder_is_empty() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
ruleRepository.add(ruleKey); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Holder has not been initialized yet"); | |||
underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_fails_with_ISE_if_branch_has_not_been_set() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
ruleRepository.add(ruleKey); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).build()); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Branch has not been set"); | |||
underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_fails_with_NPE_if_issue_has_no_key() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
ruleRepository.add(ruleKey); | |||
treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).build()); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
analysisMetadata.setBranch(mock(Branch.class)); | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("key can't be null"); | |||
underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_fails_with_NPE_if_issue_has_no_status() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey) | |||
.setKey("issueKey"); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
ruleRepository.add(ruleKey); | |||
treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).build()); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
analysisMetadata.setBranch(mock(Branch.class)); | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("newStatus can't be null"); | |||
underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
} | |||
@Test | |||
@UseDataProvider("noBranchNameBranches") | |||
public void newIssuesChangesNotification_creates_project_from_TreeRootHolder_and_branch_name_only_on_long_non_main_branches(Branch branch) { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey) | |||
.setKey("issueKey") | |||
.setStatus(STATUS_OPEN); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
ReportComponent project = ReportComponent.builder(PROJECT, 1).build(); | |||
ruleRepository.add(ruleKey); | |||
treeRootHolder.setRoot(project); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
analysisMetadata.setBranch(branch); | |||
IssuesChangesNotification expected = mock(IssuesChangesNotification.class); | |||
when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected); | |||
IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
assertThat(notification).isSameAs(expected); | |||
IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder(); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changeIssue = builder.getIssues().iterator().next(); | |||
assertThat(changeIssue.getProject().getUuid()).isEqualTo(project.getUuid()); | |||
assertThat(changeIssue.getProject().getKey()).isEqualTo(project.getKey()); | |||
assertThat(changeIssue.getProject().getProjectName()).isEqualTo(project.getName()); | |||
assertThat(changeIssue.getProject().getBranchName()).isEmpty(); | |||
} | |||
@DataProvider | |||
public static Object[][] noBranchNameBranches() { | |||
Branch mainBranch = mock(Branch.class); | |||
when(mainBranch.isMain()).thenReturn(true); | |||
when(mainBranch.isLegacyFeature()).thenReturn(false); | |||
when(mainBranch.getType()).thenReturn(BranchType.LONG); | |||
Branch legacyBranch = mock(Branch.class); | |||
when(legacyBranch.isLegacyFeature()).thenReturn(true); | |||
Branch shortBranch = mock(Branch.class); | |||
when(shortBranch.isLegacyFeature()).thenReturn(false); | |||
when(shortBranch.isMain()).thenReturn(false); | |||
when(shortBranch.getType()).thenReturn(BranchType.SHORT); | |||
Branch pr = mock(Branch.class); | |||
when(pr.isLegacyFeature()).thenReturn(false); | |||
when(pr.isMain()).thenReturn(false); | |||
when(pr.getType()).thenReturn(BranchType.PULL_REQUEST); | |||
return new Object[][] { | |||
{mainBranch}, | |||
{legacyBranch}, | |||
{shortBranch}, | |||
{pr} | |||
}; | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_creates_project_from_TreeRootHolder_and_branch_name_from_long_branch() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey) | |||
.setKey("issueKey") | |||
.setStatus(STATUS_OPEN); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
ReportComponent project = ReportComponent.builder(PROJECT, 1).build(); | |||
String branchName = randomAlphabetic(12); | |||
ruleRepository.add(ruleKey); | |||
treeRootHolder.setRoot(project); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
analysisMetadata.setBranch(newBranch(BranchType.LONG, branchName)); | |||
IssuesChangesNotification expected = mock(IssuesChangesNotification.class); | |||
when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected); | |||
IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
assertThat(notification).isSameAs(expected); | |||
IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder(); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changeIssue = builder.getIssues().iterator().next(); | |||
assertThat(changeIssue.getProject().getUuid()).isEqualTo(project.getUuid()); | |||
assertThat(changeIssue.getProject().getKey()).isEqualTo(project.getKey()); | |||
assertThat(changeIssue.getProject().getProjectName()).isEqualTo(project.getName()); | |||
assertThat(changeIssue.getProject().getBranchName()).contains(branchName); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_creates_rule_from_RuleRepository() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey) | |||
.setKey("issueKey") | |||
.setStatus(STATUS_OPEN); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
ReportComponent project = ReportComponent.builder(PROJECT, 1).build(); | |||
String branchName = randomAlphabetic(12); | |||
ruleRepository.add(ruleKey); | |||
treeRootHolder.setRoot(project); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
analysisMetadata.setBranch(newBranch(BranchType.LONG, branchName)); | |||
IssuesChangesNotification expected = mock(IssuesChangesNotification.class); | |||
when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected); | |||
IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
assertThat(notification).isSameAs(expected); | |||
IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder(); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changeIssue = builder.getIssues().iterator().next(); | |||
assertThat(changeIssue.getRule().getKey()).isEqualTo(ruleKey); | |||
assertThat(changeIssue.getRule().getName()).isEqualTo(ruleRepository.getByKey(ruleKey).getName()); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_fails_with_ISE_if_issue_has_assignee_not_in_assigneesByUuid() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
String assigneeUuid = randomAlphabetic(40); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey) | |||
.setKey("issueKey") | |||
.setStatus(STATUS_OPEN) | |||
.setAssigneeUuid(assigneeUuid); | |||
Map<String, UserDto> assigneesByUuid = Collections.emptyMap(); | |||
ReportComponent project = ReportComponent.builder(PROJECT, 1).build(); | |||
ruleRepository.add(ruleKey); | |||
treeRootHolder.setRoot(project); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
analysisMetadata.setBranch(newBranch(BranchType.LONG, randomAlphabetic(12))); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("Can not find DTO for assignee uuid " + assigneeUuid); | |||
underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_creates_assignee_from_UserDto() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
String assigneeUuid = randomAlphabetic(40); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey) | |||
.setKey("issueKey") | |||
.setStatus(STATUS_OPEN) | |||
.setAssigneeUuid(assigneeUuid); | |||
UserDto userDto = UserTesting.newUserDto(); | |||
Map<String, UserDto> assigneesByUuid = ImmutableMap.of(assigneeUuid, userDto); | |||
ReportComponent project = ReportComponent.builder(PROJECT, 1).build(); | |||
ruleRepository.add(ruleKey); | |||
treeRootHolder.setRoot(project); | |||
analysisMetadata.setAnalysisDate(new Random().nextLong()); | |||
analysisMetadata.setBranch(newBranch(BranchType.LONG, randomAlphabetic(12))); | |||
IssuesChangesNotification expected = mock(IssuesChangesNotification.class); | |||
when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected); | |||
IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
assertThat(notification).isSameAs(expected); | |||
IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder(); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changeIssue = builder.getIssues().iterator().next(); | |||
assertThat(changeIssue.getAssignee()).isPresent(); | |||
IssuesChangesNotificationBuilder.User assignee = changeIssue.getAssignee().get(); | |||
assertThat(assignee.getUuid()).isEqualTo(userDto.getUuid()); | |||
assertThat(assignee.getName()).contains(userDto.getName()); | |||
assertThat(assignee.getLogin()).isEqualTo(userDto.getLogin()); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_creates_AnalysisChange_with_analysis_date() { | |||
RuleKey ruleKey = RuleKey.of("foo", "bar"); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setRuleKey(ruleKey) | |||
.setKey("issueKey") | |||
.setStatus(STATUS_OPEN); | |||
Map<String, UserDto> assigneesByUuid = nonEmptyAssigneesByUuid(); | |||
ReportComponent project = ReportComponent.builder(PROJECT, 1).build(); | |||
long analysisDate = new Random().nextLong(); | |||
ruleRepository.add(ruleKey); | |||
treeRootHolder.setRoot(project); | |||
analysisMetadata.setAnalysisDate(analysisDate); | |||
analysisMetadata.setBranch(newBranch(BranchType.LONG, randomAlphabetic(12))); | |||
IssuesChangesNotification expected = mock(IssuesChangesNotification.class); | |||
when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected); | |||
IssuesChangesNotification notification = underTest.newIssuesChangesNotification(ImmutableSet.of(issue), assigneesByUuid); | |||
assertThat(notification).isSameAs(expected); | |||
IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder(); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
assertThat(builder.getChange()) | |||
.isInstanceOf(AnalysisChange.class) | |||
.extracting(IssuesChangesNotificationBuilder.Change::getDate) | |||
.containsOnly(analysisDate); | |||
} | |||
@Test | |||
public void newIssuesChangesNotification_maps_all_issues() { | |||
Set<DefaultIssue> issues = IntStream.range(0, 3 + new Random().nextInt(5)) | |||
.mapToObj(i -> new DefaultIssue() | |||
.setRuleKey(RuleKey.of("repo_" + i, "rule_" + i)) | |||
.setKey("issue_key_" + i) | |||
.setStatus("status_" + i)) | |||
.collect(Collectors.toSet()); | |||
ReportComponent project = ReportComponent.builder(PROJECT, 1).build(); | |||
long analysisDate = new Random().nextLong(); | |||
issues.stream() | |||
.map(DefaultIssue::ruleKey) | |||
.forEach(ruleKey -> ruleRepository.add(ruleKey)); | |||
treeRootHolder.setRoot(project); | |||
analysisMetadata.setAnalysisDate(analysisDate); | |||
analysisMetadata.setBranch(newBranch(BranchType.LONG, randomAlphabetic(12))); | |||
IssuesChangesNotification expected = mock(IssuesChangesNotification.class); | |||
when(issuesChangesSerializer.serialize(any(IssuesChangesNotificationBuilder.class))).thenReturn(expected); | |||
IssuesChangesNotification notification = underTest.newIssuesChangesNotification(issues, emptyMap()); | |||
assertThat(notification).isSameAs(expected); | |||
IssuesChangesNotificationBuilder builder = verifyAndCaptureIssueChangeNotificationBuilder(); | |||
assertThat(builder.getIssues()).hasSize(issues.size()); | |||
Map<String, ChangedIssue> changedIssuesByKey = builder.getIssues().stream() | |||
.collect(uniqueIndex(ChangedIssue::getKey)); | |||
issues.forEach( | |||
issue -> { | |||
ChangedIssue changedIssue = changedIssuesByKey.get(issue.key()); | |||
assertThat(changedIssue.getNewStatus()).isEqualTo(issue.status()); | |||
assertThat(changedIssue.getNewResolution()).isEmpty(); | |||
assertThat(changedIssue.getAssignee()).isEmpty(); | |||
assertThat(changedIssue.getRule().getKey()).isEqualTo(issue.ruleKey()); | |||
assertThat(changedIssue.getRule().getName()).isEqualTo(ruleRepository.getByKey(issue.ruleKey()).getName()); | |||
} | |||
); | |||
} | |||
private static Map<String, UserDto> nonEmptyAssigneesByUuid() { | |||
return IntStream.range(0, 1 + new Random().nextInt(3)) | |||
.boxed() | |||
.collect(uniqueIndex(i -> "uuid_" + i, i -> new UserDto())); | |||
} | |||
private IssuesChangesNotificationBuilder verifyAndCaptureIssueChangeNotificationBuilder() { | |||
ArgumentCaptor<IssuesChangesNotificationBuilder> builderCaptor = ArgumentCaptor.forClass(IssuesChangesNotificationBuilder.class); | |||
verify(issuesChangesSerializer).serialize(builderCaptor.capture()); | |||
verifyNoMoreInteractions(issuesChangesSerializer); | |||
return builderCaptor.getValue(); | |||
} | |||
private static Branch newBranch(BranchType branchType, String branchName) { | |||
Branch longBranch = mock(Branch.class); | |||
when(longBranch.isLegacyFeature()).thenReturn(false); | |||
when(longBranch.isMain()).thenReturn(false); | |||
when(longBranch.getType()).thenReturn(branchType); | |||
when(longBranch.getName()).thenReturn(branchName); | |||
return longBranch; | |||
} | |||
private static Durations readDurationsField(NewIssuesNotification notification) { | |||
return readField(notification, "durations"); | |||
} |
@@ -19,14 +19,18 @@ | |||
*/ | |||
package org.sonar.ce.task.projectanalysis.step; | |||
import com.google.common.collect.ImmutableMap; | |||
import com.google.common.collect.ImmutableSet; | |||
import java.io.IOException; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.Date; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Random; | |||
import java.util.Set; | |||
import java.util.function.Supplier; | |||
import java.util.stream.IntStream; | |||
import java.util.stream.Stream; | |||
import org.assertj.core.groups.Tuple; | |||
@@ -35,6 +39,8 @@ import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.TemporaryFolder; | |||
import org.mockito.ArgumentCaptor; | |||
import org.mockito.invocation.InvocationOnMock; | |||
import org.mockito.stubbing.Answer; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.api.utils.Duration; | |||
@@ -45,8 +51,7 @@ import org.sonar.ce.task.projectanalysis.component.Component; | |||
import org.sonar.ce.task.projectanalysis.component.DefaultBranchImpl; | |||
import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule; | |||
import org.sonar.ce.task.projectanalysis.issue.IssueCache; | |||
import org.sonar.ce.task.projectanalysis.issue.RuleRepositoryRule; | |||
import org.sonar.ce.task.projectanalysis.notification.NewIssuesNotificationFactory; | |||
import org.sonar.ce.task.projectanalysis.notification.NotificationFactory; | |||
import org.sonar.ce.task.projectanalysis.util.cache.DiskCache; | |||
import org.sonar.ce.task.step.ComputationStep; | |||
import org.sonar.ce.task.step.TestComputationStepContext; | |||
@@ -57,7 +62,7 @@ import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.notification.DistributedMetricStatsInt; | |||
import org.sonar.server.issue.notification.IssueChangeNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotification; | |||
import org.sonar.server.issue.notification.MyNewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesNotification; | |||
import org.sonar.server.issue.notification.NewIssuesStatistics; | |||
@@ -67,6 +72,7 @@ import org.sonar.server.project.Project; | |||
import static java.util.Arrays.stream; | |||
import static java.util.Collections.emptyList; | |||
import static java.util.Collections.shuffle; | |||
import static java.util.Collections.singleton; | |||
import static java.util.stream.Collectors.toList; | |||
import static java.util.stream.Stream.concat; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; | |||
@@ -75,6 +81,8 @@ import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.groups.Tuple.tuple; | |||
import static org.mockito.ArgumentCaptor.forClass; | |||
import static org.mockito.ArgumentMatchers.anyCollection; | |||
import static org.mockito.ArgumentMatchers.anyMap; | |||
import static org.mockito.ArgumentMatchers.anySet; | |||
import static org.mockito.ArgumentMatchers.eq; | |||
import static org.mockito.Mockito.any; | |||
import static org.mockito.Mockito.mock; | |||
@@ -121,8 +129,6 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
.setBranch(new DefaultBranchImpl()) | |||
.setAnalysisDate(new Date(ANALYSE_DATE)); | |||
@Rule | |||
public RuleRepositoryRule ruleRepository = new RuleRepositoryRule(); | |||
@Rule | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
@Rule | |||
public DbTester db = DbTester.create(System2.INSTANCE); | |||
@@ -132,9 +138,15 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
private final RuleType randomRuleType = RULE_TYPES_EXCEPT_HOTSPOTS[random.nextInt(RULE_TYPES_EXCEPT_HOTSPOTS.length)]; | |||
@SuppressWarnings("unchecked") | |||
private Class<Map<String, UserDto>> assigneeCacheType = (Class<Map<String, UserDto>>) (Object) Map.class; | |||
@SuppressWarnings("unchecked") | |||
private Class<Set<DefaultIssue>> setType = (Class<Set<DefaultIssue>>) (Class<?>) Set.class; | |||
@SuppressWarnings("unchecked") | |||
private Class<Map<String, UserDto>> mapType = (Class<Map<String, UserDto>>) (Class<?>) Map.class; | |||
private ArgumentCaptor<Map<String, UserDto>> assigneeCacheCaptor = ArgumentCaptor.forClass(assigneeCacheType); | |||
private ArgumentCaptor<Set<DefaultIssue>> issuesSetCaptor = forClass(setType); | |||
private ArgumentCaptor<Map<String, UserDto>> assigneeByUuidCaptor = forClass(mapType); | |||
private NotificationService notificationService = mock(NotificationService.class); | |||
private NewIssuesNotificationFactory newIssuesNotificationFactory = mock(NewIssuesNotificationFactory.class); | |||
private NotificationFactory notificationFactory = mock(NotificationFactory.class); | |||
private NewIssuesNotification newIssuesNotificationMock = createNewIssuesNotificationMock(); | |||
private MyNewIssuesNotification myNewIssuesNotificationMock = createMyNewIssuesNotificationMock(); | |||
@@ -144,10 +156,10 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
@Before | |||
public void setUp() throws Exception { | |||
issueCache = new IssueCache(temp.newFile(), System2.INSTANCE); | |||
underTest = new SendIssueNotificationsStep(issueCache, ruleRepository, treeRootHolder, notificationService, analysisMetadataHolder, | |||
newIssuesNotificationFactory, db.getDbClient()); | |||
when(newIssuesNotificationFactory.newNewIssuesNotification(any(assigneeCacheType))).thenReturn(newIssuesNotificationMock); | |||
when(newIssuesNotificationFactory.newMyNewIssuesNotification(any(assigneeCacheType))).thenReturn(myNewIssuesNotificationMock); | |||
underTest = new SendIssueNotificationsStep(issueCache, treeRootHolder, notificationService, analysisMetadataHolder, | |||
notificationFactory, db.getDbClient()); | |||
when(notificationFactory.newNewIssuesNotification(any(assigneeCacheType))).thenReturn(newIssuesNotificationMock); | |||
when(notificationFactory.newMyNewIssuesNotification(any(assigneeCacheType))).thenReturn(myNewIssuesNotificationMock); | |||
} | |||
@Test | |||
@@ -360,19 +372,19 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList())); | |||
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true); | |||
NewIssuesNotificationFactory newIssuesNotificationFactory = mock(NewIssuesNotificationFactory.class); | |||
NotificationFactory notificationFactory = mock(NotificationFactory.class); | |||
NewIssuesNotification newIssuesNotificationMock = createNewIssuesNotificationMock(); | |||
when(newIssuesNotificationFactory.newNewIssuesNotification(assigneeCacheCaptor.capture())) | |||
when(notificationFactory.newNewIssuesNotification(assigneeCacheCaptor.capture())) | |||
.thenReturn(newIssuesNotificationMock); | |||
MyNewIssuesNotification myNewIssuesNotificationMock1 = createMyNewIssuesNotificationMock(); | |||
MyNewIssuesNotification myNewIssuesNotificationMock2 = createMyNewIssuesNotificationMock(); | |||
when(newIssuesNotificationFactory.newMyNewIssuesNotification(any(assigneeCacheType))) | |||
when(notificationFactory.newMyNewIssuesNotification(any(assigneeCacheType))) | |||
.thenReturn(myNewIssuesNotificationMock1) | |||
.thenReturn(myNewIssuesNotificationMock2); | |||
TestComputationStepContext context = new TestComputationStepContext(); | |||
new SendIssueNotificationsStep(issueCache, ruleRepository, treeRootHolder, notificationService, analysisMetadataHolder, newIssuesNotificationFactory, db.getDbClient()) | |||
new SendIssueNotificationsStep(issueCache, treeRootHolder, notificationService, analysisMetadataHolder, notificationFactory, db.getDbClient()) | |||
.execute(context); | |||
verify(notificationService).deliverEmails(ImmutableSet.of(myNewIssuesNotificationMock1, myNewIssuesNotificationMock2)); | |||
@@ -380,9 +392,9 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
verify(notificationService).deliver(myNewIssuesNotificationMock1); | |||
verify(notificationService).deliver(myNewIssuesNotificationMock2); | |||
verify(newIssuesNotificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture()); | |||
verify(newIssuesNotificationFactory, times(2)).newMyNewIssuesNotification(assigneeCacheCaptor.capture()); | |||
verifyNoMoreInteractions(newIssuesNotificationFactory); | |||
verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture()); | |||
verify(notificationFactory, times(2)).newMyNewIssuesNotification(assigneeCacheCaptor.capture()); | |||
verifyNoMoreInteractions(notificationFactory); | |||
verifyAssigneeCache(assigneeCacheCaptor, perceval, arthur); | |||
Map<String, MyNewIssuesNotification> myNewIssuesNotificationMocksByUsersName = new HashMap<>(); | |||
@@ -439,10 +451,9 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
// old API compatibility | |||
verify(notificationService).deliver(myNewIssuesNotificationMock); | |||
verify(newIssuesNotificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture()); | |||
verify(newIssuesNotificationFactory).newMyNewIssuesNotification(assigneeCacheCaptor.capture()); | |||
verifyNoMoreInteractions(newIssuesNotificationFactory); | |||
verify(notificationFactory).newNewIssuesNotification(assigneeCacheCaptor.capture()); | |||
verify(notificationFactory).newMyNewIssuesNotification(assigneeCacheCaptor.capture()); | |||
verifyNoMoreInteractions(notificationFactory); | |||
verifyAssigneeCache(assigneeCacheCaptor, user); | |||
verify(myNewIssuesNotificationMock).setAssignee(any(UserDto.class)); | |||
@@ -499,7 +510,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName()); | |||
ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName()); | |||
RuleDefinitionDto ruleDefinitionDto = newRule(); | |||
DefaultIssue issue = prepareIssue(ANALYSE_DATE, user, project, file, ruleDefinitionDto, RuleType.SECURITY_HOTSPOT); | |||
prepareIssue(ANALYSE_DATE, user, project, file, ruleDefinitionDto, RuleType.SECURITY_HOTSPOT); | |||
analysisMetadataHolder.setProject(new Project(PROJECT.getUuid(), PROJECT.getKey(), PROJECT.getName(), null, emptyList())); | |||
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true); | |||
@@ -521,29 +532,33 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName()); | |||
analysisMetadataHolder.setProject(Project.from(project)); | |||
ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName()); | |||
treeRootHolder.setRoot(builder(Type.PROJECT, 2).setKey(project.getDbKey()).setPublicKey(project.getKey()).setName(project.longName()).setUuid(project.uuid()) | |||
.addChildren( | |||
builder(Type.FILE, 11).setKey(file.getDbKey()).setPublicKey(file.getKey()).setName(file.longName()).build()) | |||
.build()); | |||
RuleDefinitionDto ruleDefinitionDto = newRule(); | |||
RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)]; | |||
DefaultIssue issue = prepareIssue(issueCreatedAt, user, project, file, ruleDefinitionDto, randomTypeExceptHotspot); | |||
IssuesChangesNotification issuesChangesNotification = mock(IssuesChangesNotification.class); | |||
when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true); | |||
when(notificationFactory.newIssuesChangesNotification(anySet(), anyMap())).thenReturn(issuesChangesNotification); | |||
underTest.execute(new TestComputationStepContext()); | |||
ArgumentCaptor<IssueChangeNotification> issueChangeNotificationCaptor = forClass(IssueChangeNotification.class); | |||
verify(notificationService).deliver(issueChangeNotificationCaptor.capture()); | |||
IssueChangeNotification issueChangeNotification = issueChangeNotificationCaptor.getValue(); | |||
assertThat(issueChangeNotification.getFieldValue("key")).isEqualTo(issue.key()); | |||
assertThat(issueChangeNotification.getFieldValue("message")).isEqualTo(issue.message()); | |||
assertThat(issueChangeNotification.getFieldValue("ruleName")).isEqualTo(ruleDefinitionDto.getName()); | |||
assertThat(issueChangeNotification.getFieldValue("projectName")).isEqualTo(project.longName()); | |||
assertThat(issueChangeNotification.getFieldValue("projectKey")).isEqualTo(project.getKey()); | |||
assertThat(issueChangeNotification.getFieldValue("componentKey")).isEqualTo(file.getKey()); | |||
assertThat(issueChangeNotification.getFieldValue("componentName")).isEqualTo(file.longName()); | |||
assertThat(issueChangeNotification.getFieldValue("assignee")).isEqualTo(user.getLogin()); | |||
verify(notificationFactory).newIssuesChangesNotification(issuesSetCaptor.capture(), assigneeByUuidCaptor.capture()); | |||
assertThat(issuesSetCaptor.getValue()).hasSize(1); | |||
assertThat(issuesSetCaptor.getValue().iterator().next()).isEqualTo(issue); | |||
assertThat(assigneeByUuidCaptor.getValue()).hasSize(1); | |||
assertThat(assigneeByUuidCaptor.getValue().get(user.getUuid())).isNotNull(); | |||
verify(notificationService).hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES); | |||
verify(notificationService).deliverEmails(singleton(issuesChangesNotification)); | |||
verify(notificationService).deliver(issuesChangesNotification); | |||
verifyNoMoreInteractions(notificationService); | |||
} | |||
private DefaultIssue prepareIssue(long issueCreatedAt, UserDto user, ComponentDto project, ComponentDto file, RuleDefinitionDto ruleDefinitionDto, RuleType type) { | |||
DefaultIssue issue = newIssue(ruleDefinitionDto, project, file).setType(type).toDefaultIssue() | |||
.setNew(false).setChanged(true).setSendNotifications(true).setCreationDate(new Date(issueCreatedAt)).setAssigneeUuid(user.getUuid()); | |||
ruleRepository.add(ruleDefinitionDto.getKey()).setName(ruleDefinitionDto.getName()); | |||
issueCache.newAppender().append(issue).close(); | |||
when(notificationService.hasProjectSubscribersForTypes(project.projectUuid(), NOTIF_TYPES)).thenReturn(true); | |||
return issue; | |||
@@ -573,48 +588,85 @@ public class SendIssueNotificationsStepTest extends BaseStepTest { | |||
.setChanged(true) | |||
.setSendNotifications(true) | |||
.setCreationDate(new Date(issueCreatedAt)); | |||
ruleRepository.add(ruleDefinitionDto.getKey()).setName(ruleDefinitionDto.getName()); | |||
issueCache.newAppender().append(issue).close(); | |||
when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true); | |||
IssuesChangesNotification issuesChangesNotification = mock(IssuesChangesNotification.class); | |||
when(notificationFactory.newIssuesChangesNotification(anySet(), anyMap())).thenReturn(issuesChangesNotification); | |||
analysisMetadataHolder.setBranch(newBranch(BranchType.LONG)); | |||
underTest.execute(new TestComputationStepContext()); | |||
ArgumentCaptor<IssueChangeNotification> issueChangeNotificationCaptor = forClass(IssueChangeNotification.class); | |||
verify(notificationService).deliver(issueChangeNotificationCaptor.capture()); | |||
IssueChangeNotification issueChangeNotification = issueChangeNotificationCaptor.getValue(); | |||
assertThat(issueChangeNotification.getFieldValue("projectName")).isEqualTo(branch.longName()); | |||
assertThat(issueChangeNotification.getFieldValue("projectKey")).isEqualTo(branch.getKey()); | |||
assertThat(issueChangeNotification.getFieldValue("branch")).isEqualTo(BRANCH_NAME); | |||
assertThat(issueChangeNotification.getFieldValue("componentKey")).isEqualTo(file.getKey()); | |||
assertThat(issueChangeNotification.getFieldValue("componentName")).isEqualTo(file.longName()); | |||
verify(notificationFactory).newIssuesChangesNotification(issuesSetCaptor.capture(), assigneeByUuidCaptor.capture()); | |||
assertThat(issuesSetCaptor.getValue()).hasSize(1); | |||
assertThat(issuesSetCaptor.getValue().iterator().next()).isEqualTo(issue); | |||
assertThat(assigneeByUuidCaptor.getValue()).isEmpty(); | |||
verify(notificationService).hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES); | |||
verify(notificationService).deliverEmails(singleton(issuesChangesNotification)); | |||
verify(notificationService).deliver(issuesChangesNotification); | |||
verifyNoMoreInteractions(notificationService); | |||
} | |||
@Test | |||
public void send_issue_change_notification_in_bulks_of_1000() { | |||
public void sends_one_issue_change_notification_every_1000_issues() { | |||
UserDto user = db.users().insertUser(); | |||
ComponentDto project = newPrivateProjectDto(newOrganizationDto()).setDbKey(PROJECT.getDbKey()).setLongName(PROJECT.getName()); | |||
ComponentDto file = newFileDto(project).setDbKey(FILE.getDbKey()).setLongName(FILE.getName()); | |||
RuleDefinitionDto ruleDefinitionDto = newRule(); | |||
ruleRepository.add(ruleDefinitionDto.getKey()).setName(ruleDefinitionDto.getName()); | |||
RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)]; | |||
List<DefaultIssue> issues = IntStream.range(0, 1001 + new Random().nextInt(10)) | |||
.mapToObj(i -> newIssue(ruleDefinitionDto, project, file).setType(randomTypeExceptHotspot).toDefaultIssue() | |||
List<DefaultIssue> issues = IntStream.range(0, 2001 + new Random().nextInt(10)) | |||
.mapToObj(i -> newIssue(ruleDefinitionDto, project, file).setKee("uuid_" + i).setType(randomTypeExceptHotspot).toDefaultIssue() | |||
.setNew(false).setChanged(true).setSendNotifications(true).setAssigneeUuid(user.getUuid())) | |||
.collect(toList()); | |||
DiskCache<DefaultIssue>.DiskAppender diskAppender = issueCache.newAppender(); | |||
issues.forEach(diskAppender::append); | |||
diskAppender.close(); | |||
analysisMetadataHolder.setProject(Project.from(project)); | |||
NewIssuesFactoryCaptor newIssuesFactoryCaptor = new NewIssuesFactoryCaptor(() -> mock(IssuesChangesNotification.class)); | |||
when(notificationFactory.newIssuesChangesNotification(anySet(), anyMap())).thenAnswer(newIssuesFactoryCaptor); | |||
when(notificationService.hasProjectSubscribersForTypes(PROJECT.getUuid(), NOTIF_TYPES)).thenReturn(true); | |||
when(notificationService.hasProjectSubscribersForTypes(project.uuid(), NOTIF_TYPES)).thenReturn(true); | |||
underTest.execute(new TestComputationStepContext()); | |||
verify(notificationFactory, times(3)).newIssuesChangesNotification(anySet(), anyMap()); | |||
assertThat(newIssuesFactoryCaptor.issuesSetCaptor).hasSize(3); | |||
assertThat(newIssuesFactoryCaptor.issuesSetCaptor.get(0)).hasSize(1000); | |||
assertThat(newIssuesFactoryCaptor.issuesSetCaptor.get(1)).hasSize(1000); | |||
assertThat(newIssuesFactoryCaptor.issuesSetCaptor.get(2)).hasSize(issues.size() - 2000); | |||
assertThat(newIssuesFactoryCaptor.assigneeCacheCaptor).hasSize(3); | |||
assertThat(newIssuesFactoryCaptor.assigneeCacheCaptor).containsOnly(newIssuesFactoryCaptor.assigneeCacheCaptor.iterator().next()); | |||
ArgumentCaptor<Collection> collectionCaptor = forClass(Collection.class); | |||
verify(notificationService, times(2)).deliverEmails(collectionCaptor.capture()); | |||
verify(notificationService, times(issues.size())).deliver(any(IssueChangeNotification.class)); | |||
assertThat(collectionCaptor.getAllValues().get(0)).hasSize(1000); | |||
assertThat(collectionCaptor.getAllValues().get(1)).hasSize(issues.size() - 1000); | |||
verify(notificationService, times(3)).deliverEmails(collectionCaptor.capture()); | |||
assertThat(collectionCaptor.getAllValues()).hasSize(3); | |||
assertThat(collectionCaptor.getAllValues().get(0)).hasSize(1); | |||
assertThat(collectionCaptor.getAllValues().get(1)).hasSize(1); | |||
assertThat(collectionCaptor.getAllValues().get(2)).hasSize(1); | |||
verify(notificationService, times(3)).deliver(any(IssuesChangesNotification.class)); | |||
} | |||
/** | |||
* Since the very same Set object is passed to {@link NotificationFactory#newIssuesChangesNotification(Set, Map)} and | |||
* reset between each call. We must make a copy of each argument to capture what's been passed to the factory. | |||
* This is of course not supported by Mockito's {@link ArgumentCaptor} and we implement this ourselves with a | |||
* {@link Answer}. | |||
*/ | |||
private static class NewIssuesFactoryCaptor implements Answer<Object> { | |||
private final Supplier<IssuesChangesNotification> delegate; | |||
private final List<Set<DefaultIssue>> issuesSetCaptor = new ArrayList<>(); | |||
private final List<Map<String, UserDto>> assigneeCacheCaptor = new ArrayList<>(); | |||
private NewIssuesFactoryCaptor(Supplier<IssuesChangesNotification> delegate) { | |||
this.delegate = delegate; | |||
} | |||
@Override | |||
public Object answer(InvocationOnMock t) { | |||
Set<DefaultIssue> issuesSet = t.getArgument(0); | |||
Map<String, UserDto> assigneeCatch = t.getArgument(1); | |||
issuesSetCaptor.add(ImmutableSet.copyOf(issuesSet)); | |||
assigneeCacheCaptor.add(ImmutableMap.copyOf(assigneeCatch)); | |||
return delegate.get(); | |||
} | |||
} | |||
private NewIssuesNotification createNewIssuesNotificationMock() { |
@@ -98,9 +98,7 @@ import org.sonar.server.issue.IssueFieldsSetter; | |||
import org.sonar.server.issue.IssueStorage; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.ChangesOnMyIssueNotificationHandler; | |||
import org.sonar.server.issue.notification.DoNotFixNotificationHandler; | |||
import org.sonar.server.issue.notification.IssueChangesEmailTemplate; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationModule; | |||
import org.sonar.server.issue.notification.MyNewIssuesEmailTemplate; | |||
import org.sonar.server.issue.notification.MyNewIssuesNotificationHandler; | |||
import org.sonar.server.issue.notification.NewIssuesEmailTemplate; | |||
@@ -402,15 +400,11 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { | |||
IssueWorkflow.class, // used in Web Services and CE's DebtCalculator | |||
NewIssuesEmailTemplate.class, | |||
MyNewIssuesEmailTemplate.class, | |||
IssueChangesEmailTemplate.class, | |||
ChangesOnMyIssueNotificationHandler.class, | |||
ChangesOnMyIssueNotificationHandler.newMetadata(), | |||
NewIssuesNotificationHandler.class, | |||
NewIssuesNotificationHandler.newMetadata(), | |||
MyNewIssuesNotificationHandler.class, | |||
MyNewIssuesNotificationHandler.newMetadata(), | |||
DoNotFixNotificationHandler.class, | |||
DoNotFixNotificationHandler.newMetadata(), | |||
IssuesChangesNotificationModule.class, | |||
// Notifications | |||
QGChangeEmailTemplate.class, |
@@ -97,7 +97,8 @@ public class ComputeEngineContainerImplTest { | |||
assertThat(picoContainer.getComponentAdapters()) | |||
.hasSize( | |||
CONTAINER_ITSELF | |||
+ 67 // level 4 | |||
+ 63 // level 4 | |||
+ 7 // content of IssuesChangesNotificationModule | |||
+ 6 // content of CeConfigurationModule | |||
+ 4 // content of CeQueueModule | |||
+ 3 // content of CeHttpModule |
@@ -25,6 +25,7 @@ import java.util.Arrays; | |||
import java.util.Date; | |||
import java.util.Iterator; | |||
import java.util.Locale; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.i18n.I18n; | |||
@@ -71,6 +72,7 @@ public abstract class AbstractNewIssuesEmailTemplate implements EmailTemplate { | |||
} | |||
@Override | |||
@CheckForNull | |||
public EmailMessage format(Notification notification) { | |||
if (shouldNotFormat(notification)) { | |||
return null; | |||
@@ -102,7 +104,7 @@ public abstract class AbstractNewIssuesEmailTemplate implements EmailTemplate { | |||
return new EmailMessage() | |||
.setMessageId(notification.getType() + "/" + notification.getFieldValue(FIELD_PROJECT_KEY)) | |||
.setSubject(subject(notification, computeFullProjectName(projectName, branchName))) | |||
.setMessage(message.toString()); | |||
.setPlainTextMessage(message.toString()); | |||
} | |||
private static String computeFullProjectName(String projectName, @Nullable String branchName) { |
@@ -20,15 +20,17 @@ | |||
package org.sonar.server.issue.notification; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.google.common.collect.Multimap; | |||
import com.google.common.collect.SetMultimap; | |||
import java.util.Collection; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.Stream; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.notification.EmailNotificationHandler; | |||
import org.sonar.server.notification.NotificationDispatcherMetadata; | |||
import org.sonar.server.notification.NotificationManager; | |||
@@ -36,12 +38,12 @@ import org.sonar.server.notification.NotificationManager.EmailRecipient; | |||
import org.sonar.server.notification.email.EmailNotificationChannel; | |||
import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest; | |||
import static org.sonar.core.util.stream.MoreCollectors.index; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
import static org.sonar.core.util.stream.MoreCollectors.unorderedFlattenIndex; | |||
import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex; | |||
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER; | |||
public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandler<IssueChangeNotification> { | |||
public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandler<IssuesChangesNotification> { | |||
private static final String KEY = "ChangesOnMyIssue"; | |||
private static final NotificationDispatcherMetadata METADATA = NotificationDispatcherMetadata.create(KEY) | |||
@@ -49,10 +51,13 @@ public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandle | |||
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)); | |||
private final NotificationManager notificationManager; | |||
private final IssuesChangesNotificationSerializer serializer; | |||
public ChangesOnMyIssueNotificationHandler(NotificationManager notificationManager, EmailNotificationChannel emailNotificationChannel) { | |||
public ChangesOnMyIssueNotificationHandler(NotificationManager notificationManager, | |||
EmailNotificationChannel emailNotificationChannel, IssuesChangesNotificationSerializer serializer) { | |||
super(emailNotificationChannel); | |||
this.notificationManager = notificationManager; | |||
this.serializer = serializer; | |||
} | |||
@Override | |||
@@ -65,52 +70,110 @@ public class ChangesOnMyIssueNotificationHandler extends EmailNotificationHandle | |||
} | |||
@Override | |||
public Class<IssueChangeNotification> getNotificationClass() { | |||
return IssueChangeNotification.class; | |||
public Class<IssuesChangesNotification> getNotificationClass() { | |||
return IssuesChangesNotification.class; | |||
} | |||
@Override | |||
public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<IssueChangeNotification> notifications) { | |||
Multimap<String, IssueChangeNotification> notificationsByProjectKey = notifications.stream() | |||
// ignore inconsistent data | |||
.filter(t -> t.getProjectKey() != null) | |||
// ignore notification on which we can't identify who should be notified | |||
.filter(t -> t.getAssignee() != null) | |||
// do not notify users of the changes they made themselves (changeAuthor is null when change comes from an analysis) | |||
.filter(t -> !Objects.equals(t.getAssignee(), t.getChangeAuthor())) | |||
.collect(index(IssueChangeNotification::getProjectKey)); | |||
if (notificationsByProjectKey.isEmpty()) { | |||
public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<IssuesChangesNotification> notifications) { | |||
Set<NotificationWithProjectKeys> notificationsWithPeerChangedIssues = notifications.stream() | |||
.map(serializer::from) | |||
// ignore notification of which the changeAuthor is the assignee of all changed issues | |||
.filter(t -> t.getIssues().stream().anyMatch(issue -> issue.getAssignee().isPresent() && isPeerChanged(t.getChange(), issue))) | |||
.map(NotificationWithProjectKeys::new) | |||
.collect(Collectors.toSet()); | |||
if (notificationsWithPeerChangedIssues.isEmpty()) { | |||
return ImmutableSet.of(); | |||
} | |||
return notificationsByProjectKey.asMap().entrySet() | |||
Set<String> projectKeys = notificationsWithPeerChangedIssues.stream() | |||
.flatMap(t -> t.getProjectKeys().stream()) | |||
.collect(Collectors.toSet()); | |||
// shortcut to save from building unnecessary data structures when all changed issues in notifications belong to | |||
// the same project | |||
if (projectKeys.size() == 1) { | |||
Set<User> assigneesOfPeerChangedIssues = notificationsWithPeerChangedIssues.stream() | |||
.flatMap(t -> t.getIssues().stream().filter(issue -> isPeerChanged(t.getChange(), issue))) | |||
.map(ChangedIssue::getAssignee) | |||
.filter(Optional::isPresent) | |||
.map(Optional::get) | |||
.collect(Collectors.toSet()); | |||
Set<EmailRecipient> subscribedAssignees = notificationManager.findSubscribedEmailRecipients( | |||
KEY, | |||
projectKeys.iterator().next(), | |||
assigneesOfPeerChangedIssues.stream().map(User::getLogin).collect(Collectors.toSet()), | |||
ALL_MUST_HAVE_ROLE_USER); | |||
return subscribedAssignees.stream() | |||
.flatMap(recipient -> notificationsWithPeerChangedIssues.stream() | |||
// do not notify users of the changes they made themselves | |||
.filter(notification -> !notification.getChange().isAuthorLogin(recipient.getLogin())) | |||
.map(notification -> toEmailDeliveryRequest(notification, recipient, projectKeys))) | |||
.filter(Objects::nonNull) | |||
.collect(toSet(notificationsWithPeerChangedIssues.size())); | |||
} | |||
SetMultimap<String, String> assigneeLoginsOfPeerChangedIssuesByProjectKey = notificationsWithPeerChangedIssues.stream() | |||
.flatMap(notification -> notification.getIssues().stream() | |||
.filter(issue -> issue.getAssignee().isPresent()) | |||
.filter(issue -> isPeerChanged(notification.getChange(), issue))) | |||
.collect(unorderedIndex(t -> t.getProject().getKey(), t -> t.getAssignee().get().getLogin())); | |||
SetMultimap<String, EmailRecipient> authorizedAssigneeLoginsByProjectKey = assigneeLoginsOfPeerChangedIssuesByProjectKey.asMap().entrySet() | |||
.stream() | |||
.flatMap(e -> toEmailDeliveryRequests(e.getKey(), e.getValue())) | |||
.collect(toSet(notifications.size())); | |||
} | |||
.collect(unorderedFlattenIndex( | |||
Map.Entry::getKey, | |||
entry -> { | |||
String projectKey = entry.getKey(); | |||
Set<String> assigneeLogins = (Set<String>) entry.getValue(); | |||
return notificationManager.findSubscribedEmailRecipients(KEY, projectKey, assigneeLogins, ALL_MUST_HAVE_ROLE_USER).stream(); | |||
})); | |||
private Stream<? extends EmailDeliveryRequest> toEmailDeliveryRequests(String projectKey, Collection<IssueChangeNotification> notifications) { | |||
Set<String> assignees = notifications.stream() | |||
.map(IssueChangeNotification::getAssignee) | |||
.collect(Collectors.toSet()); | |||
Map<String, EmailRecipient> recipientsByLogin = notificationManager | |||
.findSubscribedEmailRecipients(KEY, projectKey, assignees, ALL_MUST_HAVE_ROLE_USER) | |||
SetMultimap<EmailRecipient, String> projectKeyByRecipient = authorizedAssigneeLoginsByProjectKey.entries().stream() | |||
.collect(unorderedIndex(Map.Entry::getValue, Map.Entry::getKey)); | |||
return projectKeyByRecipient.asMap().entrySet() | |||
.stream() | |||
.collect(uniqueIndex(EmailRecipient::getLogin)); | |||
return notifications.stream() | |||
.map(notification -> toEmailDeliveryRequest(recipientsByLogin, notification)) | |||
.filter(Objects::nonNull); | |||
.flatMap(entry -> { | |||
EmailRecipient recipient = entry.getKey(); | |||
Set<String> subscribedProjectKeys = (Set<String>) entry.getValue(); | |||
return notificationsWithPeerChangedIssues.stream() | |||
// do not notify users of the changes they made themselves | |||
.filter(notification -> !notification.getChange().isAuthorLogin(recipient.getLogin())) | |||
.map(notification -> toEmailDeliveryRequest(notification, recipient, subscribedProjectKeys)) | |||
.filter(Objects::nonNull); | |||
}) | |||
.collect(toSet(notificationsWithPeerChangedIssues.size())); | |||
} | |||
/** | |||
* Creates the {@link EmailDeliveryRequest} for the specified {@code recipient} with issues from the | |||
* specified {@code notification} it is the assignee of. | |||
* | |||
* @return {@code null} when the recipient is the assignee of no issue in {@code notification}. | |||
*/ | |||
@CheckForNull | |||
private static EmailNotificationChannel.EmailDeliveryRequest toEmailDeliveryRequest(Map<String, EmailRecipient> recipientsByLogin, | |||
IssueChangeNotification notification) { | |||
String assignee = notification.getAssignee(); | |||
EmailRecipient emailRecipient = recipientsByLogin.get(assignee); | |||
if (emailRecipient != null) { | |||
return new EmailNotificationChannel.EmailDeliveryRequest(emailRecipient.getEmail(), notification); | |||
private static EmailDeliveryRequest toEmailDeliveryRequest(NotificationWithProjectKeys notification, EmailRecipient recipient, Set<String> subscribedProjectKeys) { | |||
Set<ChangedIssue> recipientIssuesByProject = notification.getIssues().stream() | |||
.filter(issue -> issue.getAssignee().filter(assignee -> recipient.getLogin().equals(assignee.getLogin())).isPresent()) | |||
.filter(issue -> subscribedProjectKeys.contains(issue.getProject().getKey())) | |||
.collect(toSet(notification.getIssues().size())); | |||
if (recipientIssuesByProject.isEmpty()) { | |||
return null; | |||
} | |||
return null; | |||
return new EmailDeliveryRequest( | |||
recipient.getEmail(), | |||
new ChangesOnMyIssuesNotification(notification.getChange(), recipientIssuesByProject)); | |||
} | |||
/** | |||
* Is the author of the change the assignee of the specified issue? | |||
* If not, it means the issue has been changed by a peer of the author of the change. | |||
*/ | |||
private static boolean isPeerChanged(Change change, ChangedIssue issue) { | |||
Optional<User> assignee = issue.getAssignee(); | |||
return !assignee.isPresent() || !change.isAuthorLogin(assignee.get().getLogin()); | |||
} | |||
} |
@@ -0,0 +1,146 @@ | |||
/* | |||
* 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.ListMultimap; | |||
import com.google.common.collect.SetMultimap; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.i18n.I18n; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static org.sonar.api.issue.Issue.STATUS_CLOSED; | |||
import static org.sonar.api.issue.Issue.STATUS_OPEN; | |||
import static org.sonar.core.util.stream.MoreCollectors.index; | |||
/** | |||
* Creates email message for notification "Changes on my issues". | |||
*/ | |||
public class ChangesOnMyIssuesEmailTemplate extends IssueChangesEmailTemplate { | |||
private static final String NOTIFICATION_NAME_I18N_KEY = "notification.dispatcher.ChangesOnMyIssue"; | |||
public ChangesOnMyIssuesEmailTemplate(I18n i18n, EmailSettings settings) { | |||
super(i18n, settings); | |||
} | |||
@Override | |||
@CheckForNull | |||
public EmailMessage format(Notification notif) { | |||
if (!(notif instanceof ChangesOnMyIssuesNotification)) { | |||
return null; | |||
} | |||
ChangesOnMyIssuesNotification notification = (ChangesOnMyIssuesNotification) notif; | |||
if (notification.getChange() instanceof AnalysisChange) { | |||
checkState(!notification.getChangedIssues().isEmpty(), "changedIssues can't be empty"); | |||
return formatAnalysisNotification(notification.getChangedIssues().keySet().iterator().next(), notification); | |||
} | |||
return formatMultiProject(notification); | |||
} | |||
private EmailMessage formatAnalysisNotification(Project project, ChangesOnMyIssuesNotification notification) { | |||
return new EmailMessage() | |||
.setMessageId("changes-on-my-issues/" + project.getKey()) | |||
.setSubject(buildAnalysisSubject(project)) | |||
.setHtmlMessage(buildAnalysisMessage(project, notification)); | |||
} | |||
private static String buildAnalysisSubject(Project project) { | |||
StringBuilder res = new StringBuilder("Analysis has changed some of your issues in "); | |||
toString(res, project); | |||
return res.toString(); | |||
} | |||
private String buildAnalysisMessage(Project project, ChangesOnMyIssuesNotification notification) { | |||
String projectParams = toUrlParams(project); | |||
StringBuilder sb = new StringBuilder(); | |||
paragraph(sb, s -> s.append("Hi,")); | |||
paragraph(sb, s -> s.append("An analysis has updated ").append(issuesOrAnIssue(notification.getChangedIssues())) | |||
.append(" assigned to you:")); | |||
ListMultimap<String, ChangedIssue> issuesByNewStatus = notification.getChangedIssues().values().stream() | |||
.collect(index(changedIssue -> STATUS_CLOSED.equals(changedIssue.getNewStatus()) ? STATUS_CLOSED : STATUS_OPEN, t -> t)); | |||
List<ChangedIssue> closedIssues = issuesByNewStatus.get(STATUS_CLOSED); | |||
if (!closedIssues.isEmpty()) { | |||
paragraph(sb, s -> s.append("Closed ").append(issueOrIssues(closedIssues)).append(":")); | |||
addIssuesByRule(sb, closedIssues, projectIssuePageHref(projectParams)); | |||
} | |||
List<ChangedIssue> openIssues = issuesByNewStatus.get(STATUS_OPEN); | |||
if (!openIssues.isEmpty()) { | |||
paragraph(sb, s -> s.append("Open ").append(issueOrIssues(openIssues)).append(":")); | |||
addIssuesByRule(sb, openIssues, projectIssuePageHref(projectParams)); | |||
} | |||
addFooter(sb, NOTIFICATION_NAME_I18N_KEY); | |||
return sb.toString(); | |||
} | |||
private EmailMessage formatMultiProject(ChangesOnMyIssuesNotification notification) { | |||
User user = ((UserChange) notification.getChange()).getUser(); | |||
return new EmailMessage() | |||
.setFrom(user.getName().orElse(user.getLogin())) | |||
.setMessageId("changes-on-my-issues") | |||
.setSubject("A manual update has changed some of your issues") | |||
.setHtmlMessage(buildMultiProjectMessage(notification)); | |||
} | |||
private String buildMultiProjectMessage(ChangesOnMyIssuesNotification notification) { | |||
StringBuilder sb = new StringBuilder(); | |||
paragraph(sb, s -> s.append("Hi,")); | |||
paragraph(sb, s -> { | |||
SetMultimap<Project, ChangedIssue> changedIssues = notification.getChangedIssues(); | |||
s.append("A manual change has updated ").append(issuesOrAnIssue(changedIssues)) | |||
.append(" assigned to you:"); | |||
}); | |||
addIssuesByProjectThenRule(sb, notification.getChangedIssues()); | |||
addFooter(sb, NOTIFICATION_NAME_I18N_KEY); | |||
return sb.toString(); | |||
} | |||
private static String issueOrIssues(Collection<?> collection) { | |||
if (collection.size() > 1) { | |||
return "issues"; | |||
} | |||
return "issue"; | |||
} | |||
private static String issuesOrAnIssue(SetMultimap<Project, ChangedIssue> changedIssues) { | |||
if (changedIssues.size() > 1) { | |||
return "issues"; | |||
} | |||
return "an issue"; | |||
} | |||
} |
@@ -0,0 +1,74 @@ | |||
/* | |||
* 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.SetMultimap; | |||
import java.util.Collection; | |||
import java.util.Objects; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex; | |||
/** | |||
* This notification is never serialized to DB. | |||
* <p> | |||
* It is derived from {@link IssuesChangesNotification} by | |||
* {@link FPOrWontFixNotificationHandler} and extends {@link Notification} only to comply with | |||
* {@link org.sonar.server.issue.notification.EmailTemplate#format(Notification)} API. | |||
*/ | |||
class ChangesOnMyIssuesNotification extends Notification { | |||
private final Change change; | |||
private final SetMultimap<Project, ChangedIssue> changedIssues; | |||
public ChangesOnMyIssuesNotification(Change change, Collection<ChangedIssue> changedIssues) { | |||
super("ChangesOnMyIssues"); | |||
this.change = change; | |||
this.changedIssues = changedIssues.stream().collect(unorderedIndex(ChangedIssue::getProject, t -> t)); | |||
} | |||
public Change getChange() { | |||
return change; | |||
} | |||
public SetMultimap<Project, ChangedIssue> getChangedIssues() { | |||
return changedIssues; | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
ChangesOnMyIssuesNotification that = (ChangesOnMyIssuesNotification) o; | |||
return Objects.equals(change, that.change) && | |||
Objects.equals(changedIssues, that.changedIssues); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(change, changedIssues); | |||
} | |||
} |
@@ -1,104 +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.ImmutableSet; | |||
import com.google.common.collect.Multimap; | |||
import java.util.Collection; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.stream.Stream; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.server.notification.EmailNotificationHandler; | |||
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.Optional.of; | |||
import static org.sonar.core.util.stream.MoreCollectors.index; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER; | |||
public class DoNotFixNotificationHandler extends EmailNotificationHandler<IssueChangeNotification> { | |||
public static final String KEY = "NewFalsePositiveIssue"; | |||
private static final NotificationDispatcherMetadata METADATA = NotificationDispatcherMetadata.create(KEY) | |||
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(false)) | |||
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)); | |||
private static final Set<String> SUPPORTED_NEW_RESOLUTIONS = ImmutableSet.of(Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_WONT_FIX); | |||
private final NotificationManager notificationManager; | |||
public DoNotFixNotificationHandler(NotificationManager notificationManager, EmailNotificationChannel emailNotificationChannel) { | |||
super(emailNotificationChannel); | |||
this.notificationManager = notificationManager; | |||
} | |||
@Override | |||
public Optional<NotificationDispatcherMetadata> getMetadata() { | |||
return of(METADATA); | |||
} | |||
public static NotificationDispatcherMetadata newMetadata() { | |||
return METADATA; | |||
} | |||
@Override | |||
public Class<IssueChangeNotification> getNotificationClass() { | |||
return IssueChangeNotification.class; | |||
} | |||
@Override | |||
public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<IssueChangeNotification> notifications) { | |||
Multimap<String, IssueChangeNotification> notificationsByProjectKey = notifications.stream() | |||
// ignore inconsistent data | |||
.filter(t -> t.getProjectKey() != null) | |||
// ignore notification on which we can't identify who should not be notified | |||
// (and anyway, it should not be null as an analysis can not resolve an issue as FP or Won't fix) | |||
.filter(t -> t.getChangeAuthor() != null) | |||
// ignore changes which did not lead to a FP or Won't Fix resolution | |||
.filter(t -> SUPPORTED_NEW_RESOLUTIONS.contains(t.getNewResolution())) | |||
.collect(index(IssueChangeNotification::getProjectKey)); | |||
if (notificationsByProjectKey.isEmpty()) { | |||
return emptySet(); | |||
} | |||
return notificationsByProjectKey.asMap().entrySet() | |||
.stream() | |||
.flatMap(e -> toEmailDeliveryRequests(e.getKey(), e.getValue())) | |||
.collect(toSet(notifications.size())); | |||
} | |||
private Stream<? extends EmailDeliveryRequest> toEmailDeliveryRequests(String projectKey, Collection<IssueChangeNotification> notifications) { | |||
Set<EmailRecipient> recipients = notificationManager | |||
.findSubscribedEmailRecipients(KEY, projectKey, ALL_MUST_HAVE_ROLE_USER); | |||
return notifications.stream() | |||
.flatMap(notification -> recipients.stream() | |||
// do not notify author of the change | |||
.filter(t -> !Objects.equals(t.getLogin(), notification.getChangeAuthor())) | |||
.map(t -> new EmailDeliveryRequest(t.getEmail(), notification))); | |||
} | |||
} |
@@ -23,11 +23,12 @@ import org.apache.commons.lang.builder.ToStringBuilder; | |||
public class EmailMessage { | |||
private String from; | |||
private String to; | |||
private String subject; | |||
private String message; | |||
private String messageId; | |||
private String from = null; | |||
private String to = null; | |||
private String subject = null; | |||
private String message = null; | |||
private boolean html = false; | |||
private String messageId = null; | |||
/** | |||
* @param from full name of user, who initiated this message or null, if message was initiated by Sonar | |||
@@ -77,13 +78,25 @@ public class EmailMessage { | |||
/** | |||
* @param message message body | |||
*/ | |||
public EmailMessage setMessage(String message) { | |||
public EmailMessage setPlainTextMessage(String message) { | |||
this.message = message; | |||
this.html = false; | |||
return this; | |||
} | |||
/** | |||
* @see #setMessage(String) | |||
* @param message HTML message body | |||
*/ | |||
public EmailMessage setHtmlMessage(String message) { | |||
this.message = message; | |||
this.html = true; | |||
return this; | |||
} | |||
/** | |||
* Either plain text or HTML. | |||
* @see #setPlainTextMessage(String) (String) | |||
* @see #setHtmlMessage(String) (String) (String) | |||
*/ | |||
public String getMessage() { | |||
return message; | |||
@@ -104,6 +117,10 @@ public class EmailMessage { | |||
return messageId; | |||
} | |||
public boolean isHtml() { | |||
return html; | |||
} | |||
@Override | |||
public String toString() { | |||
return ToStringBuilder.reflectionToString(this); |
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.ExtensionPoint; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.api.notifications.Notification; | |||
@@ -27,6 +28,7 @@ import org.sonar.api.notifications.Notification; | |||
@ExtensionPoint | |||
public interface EmailTemplate { | |||
@CheckForNull | |||
EmailMessage format(Notification notification); | |||
} |
@@ -0,0 +1,96 @@ | |||
/* | |||
* 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.SetMultimap; | |||
import java.util.Collection; | |||
import java.util.Objects; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex; | |||
/** | |||
* This notification is never serialized to DB. | |||
* <p> | |||
* It is derived from {@link IssuesChangesNotification} by | |||
* {@link FPOrWontFixNotificationHandler} and extends {@link Notification} only to comply with | |||
* {@link org.sonar.server.issue.notification.EmailTemplate#format(Notification)} API. | |||
*/ | |||
class FPOrWontFixNotification extends Notification { | |||
private static final String KEY = "FPorWontFix"; | |||
public enum FpOrWontFix { | |||
FP, WONT_FIX | |||
} | |||
private final Change change; | |||
private final SetMultimap<Project, ChangedIssue> changedIssues; | |||
private final FpOrWontFix resolution; | |||
public FPOrWontFixNotification(Change change, Collection<ChangedIssue> changedIssues, FpOrWontFix resolution) { | |||
super(KEY); | |||
this.changedIssues = changedIssues.stream().collect(unorderedIndex(ChangedIssue::getProject, t -> t)); | |||
this.change = change; | |||
this.resolution = resolution; | |||
} | |||
public Change getChange() { | |||
return change; | |||
} | |||
public SetMultimap<Project, ChangedIssue> getChangedIssues() { | |||
return changedIssues; | |||
} | |||
public FpOrWontFix getResolution() { | |||
return resolution; | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
FPOrWontFixNotification that = (FPOrWontFixNotification) o; | |||
return Objects.equals(changedIssues, that.changedIssues) && | |||
Objects.equals(change, that.change) && | |||
resolution == that.resolution; | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(changedIssues, change, resolution); | |||
} | |||
@Override | |||
public String toString() { | |||
return "FPOrWontFixNotification{" + | |||
"changedIssues=" + changedIssues + | |||
", change=" + change + | |||
", resolution=" + resolution + | |||
'}'; | |||
} | |||
} |
@@ -0,0 +1,172 @@ | |||
/* | |||
* 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 com.google.common.collect.Multimap; | |||
import com.google.common.collect.SetMultimap; | |||
import java.util.Collection; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.Stream; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.notification.EmailNotificationHandler; | |||
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 com.google.common.collect.Sets.intersection; | |||
import static java.util.Collections.emptySet; | |||
import static java.util.Optional.of; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.core.util.stream.MoreCollectors.unorderedFlattenIndex; | |||
import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex; | |||
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP; | |||
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX; | |||
import static org.sonar.server.notification.NotificationManager.SubscriberPermissionsOnProject.ALL_MUST_HAVE_ROLE_USER; | |||
public class FPOrWontFixNotificationHandler extends EmailNotificationHandler<IssuesChangesNotification> { | |||
public static final String KEY = "NewFalsePositiveIssue"; | |||
private static final NotificationDispatcherMetadata METADATA = NotificationDispatcherMetadata.create(KEY) | |||
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(false)) | |||
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)); | |||
private static final Set<String> FP_OR_WONTFIX_RESOLUTIONS = ImmutableSet.of(Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_WONT_FIX); | |||
private final NotificationManager notificationManager; | |||
private final IssuesChangesNotificationSerializer serializer; | |||
public FPOrWontFixNotificationHandler(NotificationManager notificationManager, | |||
EmailNotificationChannel emailNotificationChannel, IssuesChangesNotificationSerializer serializer) { | |||
super(emailNotificationChannel); | |||
this.notificationManager = notificationManager; | |||
this.serializer = serializer; | |||
} | |||
@Override | |||
public Optional<NotificationDispatcherMetadata> getMetadata() { | |||
return of(METADATA); | |||
} | |||
public static NotificationDispatcherMetadata newMetadata() { | |||
return METADATA; | |||
} | |||
@Override | |||
public Class<IssuesChangesNotification> getNotificationClass() { | |||
return IssuesChangesNotification.class; | |||
} | |||
@Override | |||
public Set<EmailDeliveryRequest> toEmailDeliveryRequests(Collection<IssuesChangesNotification> notifications) { | |||
Set<NotificationWithProjectKeys> changeNotificationsWithFpOrWontFix = notifications.stream() | |||
.map(serializer::from) | |||
// ignore notifications which contain no issue changed to a FP or Won't Fix resolution | |||
.filter(t -> t.getIssues().stream() | |||
.filter(issue -> issue.getNewResolution().isPresent()) | |||
.anyMatch(issue -> FP_OR_WONTFIX_RESOLUTIONS.contains(issue.getNewResolution().get()))) | |||
.map(NotificationWithProjectKeys::new) | |||
.collect(Collectors.toSet()); | |||
if (changeNotificationsWithFpOrWontFix.isEmpty()) { | |||
return emptySet(); | |||
} | |||
Set<String> projectKeys = changeNotificationsWithFpOrWontFix.stream() | |||
.flatMap(t -> t.getProjectKeys().stream()) | |||
.collect(Collectors.toSet()); | |||
// shortcut to save from building unnecessary data structures when all changed issues in notifications belong to | |||
// the same project | |||
if (projectKeys.size() == 1) { | |||
Set<EmailRecipient> recipients = notificationManager.findSubscribedEmailRecipients(KEY, projectKeys.iterator().next(), ALL_MUST_HAVE_ROLE_USER); | |||
return changeNotificationsWithFpOrWontFix.stream() | |||
.flatMap(notification -> toRequests(notification, projectKeys, recipients)) | |||
.collect(toSet(changeNotificationsWithFpOrWontFix.size())); | |||
} | |||
Set<EmailRecipientAndProject> recipientsByProjectKey = projectKeys.stream() | |||
.flatMap(projectKey -> notificationManager.findSubscribedEmailRecipients(KEY, projectKey, ALL_MUST_HAVE_ROLE_USER).stream() | |||
.map(emailRecipient -> new EmailRecipientAndProject(emailRecipient, projectKey))) | |||
.collect(Collectors.toSet()); | |||
// builds sets of projectKeys for which a given recipient has subscribed to | |||
SetMultimap<EmailRecipient, String> projectKeysByRecipient = recipientsByProjectKey.stream() | |||
.collect(unorderedIndex(t -> t.recipient, t -> t.projectKey)); | |||
// builds sets of recipients who subscribed to the same subset of projects | |||
Multimap<Set<String>, EmailRecipient> recipientsBySubscribedProjects = projectKeysByRecipient.asMap() | |||
.entrySet().stream() | |||
.collect(unorderedIndex(t -> (Set<String>) t.getValue(), Map.Entry::getKey)); | |||
return changeNotificationsWithFpOrWontFix.stream() | |||
.flatMap(notification -> { | |||
// builds sets of recipients for each sub group of the notification's projectKeys necessary | |||
SetMultimap<Set<String>, EmailRecipient> recipientsByProjectKeys = recipientsBySubscribedProjects.asMap().entrySet() | |||
.stream() | |||
.collect(unorderedFlattenIndex(t -> intersection(t.getKey(), notification.getProjectKeys()).immutableCopy(), t -> t.getValue().stream())); | |||
return recipientsByProjectKeys.asMap().entrySet().stream() | |||
.flatMap(entry -> toRequests(notification, entry.getKey(), entry.getValue())); | |||
}) | |||
.collect(toSet(changeNotificationsWithFpOrWontFix.size())); | |||
} | |||
private static Stream<EmailDeliveryRequest> toRequests(NotificationWithProjectKeys notification, Set<String> projectKeys, Collection<EmailRecipient> recipients) { | |||
return recipients.stream() | |||
// do not notify author of the change | |||
.filter(recipient -> !notification.getChange().isAuthorLogin(recipient.getLogin())) | |||
.flatMap(recipient -> { | |||
SetMultimap<String, ChangedIssue> issuesByNewResolution = notification.getIssues().stream() | |||
// ignore issues not changed to a FP or Won't Fix resolution | |||
.filter(issue -> issue.getNewResolution().filter(FP_OR_WONTFIX_RESOLUTIONS::contains).isPresent()) | |||
// ignore issues belonging to projects the recipients have not subscribed to | |||
.filter(issue -> projectKeys.contains(issue.getProject().getKey())) | |||
.collect(unorderedIndex(t -> t.getNewResolution().get(), issue -> issue)); | |||
return Stream.of( | |||
ofNullable(issuesByNewResolution.get(Issue.RESOLUTION_FALSE_POSITIVE)) | |||
.filter(t -> !t.isEmpty()) | |||
.map(fpIssues -> new FPOrWontFixNotification(notification.getChange(), fpIssues, FP)) | |||
.orElse(null), | |||
ofNullable(issuesByNewResolution.get(Issue.RESOLUTION_WONT_FIX)) | |||
.filter(t -> !t.isEmpty()) | |||
.map(wontFixIssues -> new FPOrWontFixNotification(notification.getChange(), wontFixIssues, WONT_FIX)) | |||
.orElse(null)) | |||
.filter(Objects::nonNull) | |||
.map(fpOrWontFixNotification -> new EmailDeliveryRequest(recipient.getEmail(), fpOrWontFixNotification)); | |||
}); | |||
} | |||
private static final class EmailRecipientAndProject { | |||
private final EmailRecipient recipient; | |||
private final String projectKey; | |||
private EmailRecipientAndProject(EmailRecipient recipient, String projectKey) { | |||
this.recipient = recipient; | |||
this.projectKey = projectKey; | |||
} | |||
} | |||
} |
@@ -0,0 +1,101 @@ | |||
/* | |||
* 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 javax.annotation.CheckForNull; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.i18n.I18n; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP; | |||
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX; | |||
/** | |||
* Creates email message for notification "issue-changes". | |||
*/ | |||
public class FpOrWontFixEmailTemplate extends IssueChangesEmailTemplate { | |||
private static final String NOTIFICATION_NAME_I18N_KEY = "notification.dispatcher.NewFalsePositiveIssue"; | |||
public FpOrWontFixEmailTemplate(I18n i18n, EmailSettings settings) { | |||
super(i18n, settings); | |||
} | |||
@Override | |||
@CheckForNull | |||
public EmailMessage format(Notification notif) { | |||
if (!(notif instanceof FPOrWontFixNotification)) { | |||
return null; | |||
} | |||
FPOrWontFixNotification notification = (FPOrWontFixNotification) notif; | |||
EmailMessage emailMessage = new EmailMessage() | |||
.setMessageId(getMessageId(notification.getResolution())) | |||
.setSubject(buildSubject(notification)) | |||
.setHtmlMessage(buildMessage(notification)); | |||
if (notification.getChange() instanceof UserChange) { | |||
User user = ((UserChange) notification.getChange()).getUser(); | |||
emailMessage.setFrom(user.getName().orElse(user.getLogin())); | |||
} | |||
return emailMessage; | |||
} | |||
private static String getMessageId(FpOrWontFix resolution) { | |||
if (resolution == WONT_FIX) { | |||
return "wontfix-issue-changes"; | |||
} | |||
if (resolution == FP) { | |||
return "fp-issue-changes"; | |||
} | |||
throw new IllegalArgumentException("Unsupported resolution " + resolution); | |||
} | |||
private static String buildSubject(FPOrWontFixNotification notification) { | |||
return "Issues marked as " + resolutionLabel(notification.getResolution()); | |||
} | |||
private String buildMessage(FPOrWontFixNotification notification) { | |||
StringBuilder sb = new StringBuilder(); | |||
paragraph(sb, s -> s.append("Hi,")); | |||
paragraph(sb, s -> s.append("A manual change has resolved ").append(notification.getChangedIssues().size() > 1 ? "issues" : "an issue") | |||
.append(" as ").append(resolutionLabel(notification.getResolution())).append(":")); | |||
addIssuesByProjectThenRule(sb, notification.getChangedIssues()); | |||
addFooter(sb, NOTIFICATION_NAME_I18N_KEY); | |||
return sb.toString(); | |||
} | |||
private static String resolutionLabel(FpOrWontFix resolution) { | |||
if (resolution == WONT_FIX) { | |||
return "Won't Fix"; | |||
} | |||
if (resolution == FP) { | |||
return "False Positive"; | |||
} | |||
throw new IllegalArgumentException("Unsupported resolution " + resolution); | |||
} | |||
} |
@@ -1,142 +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.base.Strings; | |||
import java.io.Serializable; | |||
import java.util.Map; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.FieldDiffs; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.user.UserDto; | |||
import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_BRANCH; | |||
import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PROJECT_KEY; | |||
import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PROJECT_NAME; | |||
import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PULL_REQUEST; | |||
public class IssueChangeNotification extends Notification { | |||
public static final String TYPE = "issue-changes"; | |||
private static final String FIELD_CHANGE_AUTHOR = "changeAuthor"; | |||
private static final String FIELD_ASSIGNEE = "assignee"; | |||
public IssueChangeNotification() { | |||
super(TYPE); | |||
} | |||
public IssueChangeNotification setIssue(DefaultIssue issue) { | |||
setFieldValue("key", issue.key()); | |||
setFieldValue("message", issue.message()); | |||
FieldDiffs currentChange = issue.currentChange(); | |||
if (currentChange != null) { | |||
for (Map.Entry<String, FieldDiffs.Diff> entry : currentChange.diffs().entrySet()) { | |||
String type = entry.getKey(); | |||
FieldDiffs.Diff diff = entry.getValue(); | |||
setFieldValue("old." + type, neverEmptySerializableToString(diff.oldValue())); | |||
setFieldValue("new." + type, neverEmptySerializableToString(diff.newValue())); | |||
} | |||
} | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getNewResolution() { | |||
return getFieldValue("new.resolution"); | |||
} | |||
public IssueChangeNotification setProject(ComponentDto project) { | |||
return setProject(project.getKey(), project.name(), project.getBranch(), project.getPullRequest()); | |||
} | |||
public IssueChangeNotification setProject(String projectKey, String projectName, @Nullable String branch, @Nullable String pullRequest) { | |||
setFieldValue(FIELD_PROJECT_NAME, projectName); | |||
setFieldValue(FIELD_PROJECT_KEY, projectKey); | |||
if (branch != null) { | |||
setFieldValue(FIELD_BRANCH, branch); | |||
} | |||
if (pullRequest != null) { | |||
setFieldValue(FIELD_PULL_REQUEST, pullRequest); | |||
} | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getProjectKey() { | |||
return getFieldValue(FIELD_PROJECT_KEY); | |||
} | |||
public IssueChangeNotification setComponent(ComponentDto component) { | |||
return setComponent(component.getKey(), component.longName()); | |||
} | |||
public IssueChangeNotification setComponent(String componentKey, String componentName) { | |||
setFieldValue("componentName", componentName); | |||
setFieldValue("componentKey", componentKey); | |||
return this; | |||
} | |||
public IssueChangeNotification setChangeAuthor(@Nullable UserDto author) { | |||
if (author == null) { | |||
return this; | |||
} | |||
setFieldValue(FIELD_CHANGE_AUTHOR, author.getLogin()); | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getChangeAuthor() { | |||
return getFieldValue(FIELD_CHANGE_AUTHOR); | |||
} | |||
public IssueChangeNotification setRuleName(@Nullable String s) { | |||
if (s != null) { | |||
setFieldValue("ruleName", s); | |||
} | |||
return this; | |||
} | |||
public IssueChangeNotification setComment(@Nullable String s) { | |||
if (s != null) { | |||
setFieldValue("comment", s); | |||
} | |||
return this; | |||
} | |||
@CheckForNull | |||
private static String neverEmptySerializableToString(@Nullable Serializable s) { | |||
return s != null ? Strings.emptyToNull(s.toString()) : null; | |||
} | |||
public IssueChangeNotification setAssignee(@Nullable UserDto assignee) { | |||
if (assignee != null) { | |||
setFieldValue(FIELD_ASSIGNEE, assignee.getLogin()); | |||
} | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getAssignee() { | |||
return getFieldValue(FIELD_ASSIGNEE); | |||
} | |||
} |
@@ -19,162 +19,190 @@ | |||
*/ | |||
package org.sonar.server.issue.notification; | |||
import com.google.common.base.Strings; | |||
import com.google.common.collect.ImmutableList; | |||
import com.google.common.collect.ImmutableSortedSet; | |||
import com.google.common.collect.ListMultimap; | |||
import com.google.common.collect.Lists; | |||
import com.google.common.collect.SetMultimap; | |||
import java.io.UnsupportedEncodingException; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.lang.StringUtils; | |||
import java.util.Collection; | |||
import java.util.Comparator; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
import java.util.Locale; | |||
import java.util.Optional; | |||
import java.util.SortedSet; | |||
import java.util.function.BiConsumer; | |||
import java.util.function.Consumer; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.api.i18n.I18n; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule; | |||
import static java.net.URLEncoder.encode; | |||
import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_BRANCH; | |||
import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PULL_REQUEST; | |||
/** | |||
* Creates email message for notification "issue-changes". | |||
*/ | |||
public class IssueChangesEmailTemplate implements EmailTemplate { | |||
private static final char NEW_LINE = '\n'; | |||
private final DbClient dbClient; | |||
import static org.sonar.core.util.stream.MoreCollectors.index; | |||
public abstract class IssueChangesEmailTemplate implements EmailTemplate { | |||
private static final Comparator<Rule> RULE_COMPARATOR = Comparator.comparing(r -> r.getKey().toString()); | |||
private static final Comparator<Project> PROJECT_COMPARATOR = Comparator.comparing(Project::getProjectName) | |||
.thenComparing(t -> t.getBranchName().orElse("")); | |||
private static final Comparator<ChangedIssue> CHANGED_ISSUE_KEY_COMPARATOR = Comparator.comparing(ChangedIssue::getKey, Comparator.naturalOrder()); | |||
/** | |||
* Assuming: | |||
* <ul> | |||
* <li>UUID length of 40 chars</li> | |||
* <li>a max URL length of 2083 chars</li> | |||
* </ul> | |||
* This leaves ~850 chars for the rest of the URL (including other parameters such as the project key and the branch), | |||
* which is reasonable to stay safe from the max URL length supported by some browsers and network devices. | |||
*/ | |||
private static final int MAX_ISSUES_BY_LINK = 40; | |||
private static final String URL_ENCODED_COMMA = urlEncode(","); | |||
private final I18n i18n; | |||
private final EmailSettings settings; | |||
public IssueChangesEmailTemplate(DbClient dbClient, EmailSettings settings) { | |||
this.dbClient = dbClient; | |||
protected IssueChangesEmailTemplate(I18n i18n, EmailSettings settings) { | |||
this.i18n = i18n; | |||
this.settings = settings; | |||
} | |||
@Override | |||
public EmailMessage format(Notification notif) { | |||
if (!IssueChangeNotification.TYPE.equals(notif.getType())) { | |||
return null; | |||
} | |||
StringBuilder sb = new StringBuilder(); | |||
appendHeader(notif, sb); | |||
sb.append(NEW_LINE); | |||
appendChanges(notif, sb); | |||
sb.append(NEW_LINE); | |||
appendFooter(sb, notif); | |||
String projectName = notif.getFieldValue("projectName"); | |||
String issueKey = notif.getFieldValue("key"); | |||
String author = notif.getFieldValue("changeAuthor"); | |||
EmailMessage message = new EmailMessage() | |||
.setMessageId("issue-changes/" + issueKey) | |||
.setSubject(projectName + ", change on issue #" + issueKey) | |||
.setMessage(sb.toString()); | |||
if (author != null) { | |||
message.setFrom(getUserFullName(author)); | |||
/** | |||
* Adds "projectName" or "projectName, branchName" if branchName is non null | |||
*/ | |||
protected static void toString(StringBuilder sb, Project project) { | |||
Optional<String> branchName = project.getBranchName(); | |||
if (branchName.isPresent()) { | |||
sb.append(project.getProjectName()).append(", ").append(branchName.get()); | |||
} else { | |||
sb.append(project.getProjectName()); | |||
} | |||
return message; | |||
} | |||
private static void appendChanges(Notification notif, StringBuilder sb) { | |||
appendField(sb, "Comment", null, notif.getFieldValue("comment")); | |||
appendFieldWithoutHistory(sb, "Assignee", notif.getFieldValue("old.assignee"), notif.getFieldValue("new.assignee")); | |||
appendField(sb, "Severity", notif.getFieldValue("old.severity"), notif.getFieldValue("new.severity")); | |||
appendField(sb, "Type", notif.getFieldValue("old.type"), notif.getFieldValue("new.type")); | |||
appendField(sb, "Resolution", notif.getFieldValue("old.resolution"), notif.getFieldValue("new.resolution")); | |||
appendField(sb, "Status", notif.getFieldValue("old.status"), notif.getFieldValue("new.status")); | |||
appendField(sb, "Message", notif.getFieldValue("old.message"), notif.getFieldValue("new.message")); | |||
appendField(sb, "Author", notif.getFieldValue("old.author"), notif.getFieldValue("new.author")); | |||
appendFieldWithoutHistory(sb, "Action Plan", notif.getFieldValue("old.actionPlan"), notif.getFieldValue("new.actionPlan")); | |||
appendField(sb, "Tags", formatTagChange(notif.getFieldValue("old.tags")), formatTagChange(notif.getFieldValue("new.tags"))); | |||
static String toUrlParams(Project project) { | |||
return "id=" + urlEncode(project.getKey()) + | |||
project.getBranchName().map(branchName -> "&branch=" + urlEncode(branchName)).orElse(""); | |||
} | |||
@CheckForNull | |||
private static String formatTagChange(@Nullable String tags) { | |||
if (tags == null) { | |||
return null; | |||
} else { | |||
return "[" + tags + "]"; | |||
} | |||
void addIssuesByProjectThenRule(StringBuilder sb, SetMultimap<Project, ChangedIssue> issuesByProject) { | |||
issuesByProject.keySet().stream() | |||
.sorted(PROJECT_COMPARATOR) | |||
.forEach(project -> { | |||
String encodedProjectParams = toUrlParams(project); | |||
paragraph(sb, s -> toString(s, project)); | |||
addIssuesByRule(sb, issuesByProject.get(project), projectIssuePageHref(encodedProjectParams)); | |||
}); | |||
} | |||
private static void appendHeader(Notification notif, StringBuilder sb) { | |||
appendLine(sb, StringUtils.defaultString(notif.getFieldValue("componentName"), notif.getFieldValue("componentKey"))); | |||
String branchName = notif.getFieldValue(FIELD_BRANCH); | |||
if (branchName != null) { | |||
appendField(sb, "Branch", null, branchName); | |||
} | |||
String pullRequest = notif.getFieldValue(FIELD_PULL_REQUEST); | |||
if (pullRequest != null) { | |||
appendField(sb, "Pull request", null, pullRequest); | |||
void addIssuesByRule(StringBuilder sb, Collection<ChangedIssue> changedIssues, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref) { | |||
ListMultimap<Rule, ChangedIssue> issuesByRule = changedIssues.stream() | |||
.collect(index(ChangedIssue::getRule, t -> t)); | |||
Iterator<Rule> rules = issuesByRule.keySet().stream() | |||
.sorted(RULE_COMPARATOR) | |||
.iterator(); | |||
if (!rules.hasNext()) { | |||
return; | |||
} | |||
appendField(sb, "Rule", null, notif.getFieldValue("ruleName")); | |||
appendField(sb, "Message", null, notif.getFieldValue("message")); | |||
} | |||
private void appendFooter(StringBuilder sb, Notification notification) { | |||
String issueKey = notification.getFieldValue("key"); | |||
try { | |||
sb.append("More details at: ").append(settings.getServerBaseURL()) | |||
.append("/project/issues?id=").append(encode(notification.getFieldValue("projectKey"), "UTF-8")) | |||
.append("&issues=").append(issueKey) | |||
.append("&open=").append(issueKey); | |||
String branchName = notification.getFieldValue(FIELD_BRANCH); | |||
if (branchName != null) { | |||
sb.append("&branch=").append(branchName); | |||
} | |||
String pullRequest = notification.getFieldValue(FIELD_PULL_REQUEST); | |||
if (pullRequest != null) { | |||
sb.append("&pullRequest=").append(pullRequest); | |||
} | |||
sb.append(NEW_LINE); | |||
} catch (UnsupportedEncodingException e) { | |||
throw new IllegalStateException("Encoding not supported", e); | |||
sb.append("<ul>"); | |||
while (rules.hasNext()) { | |||
Rule rule = rules.next(); | |||
Collection<ChangedIssue> issues = issuesByRule.get(rule); | |||
sb.append("<li>").append("Rule ").append(" <em>").append(rule.getName()).append("</em> - "); | |||
appendIssueLinks(sb, issuePageHref, issues); | |||
sb.append("</li>"); | |||
} | |||
sb.append("</ul>"); | |||
} | |||
private static void appendLine(StringBuilder sb, @Nullable String line) { | |||
if (!Strings.isNullOrEmpty(line)) { | |||
sb.append(line).append(NEW_LINE); | |||
private static void appendIssueLinks(StringBuilder sb, BiConsumer<StringBuilder, Collection<ChangedIssue>> issuePageHref, Collection<ChangedIssue> issues) { | |||
SortedSet<ChangedIssue> sortedIssues = ImmutableSortedSet.copyOf(CHANGED_ISSUE_KEY_COMPARATOR, issues); | |||
int issueCount = issues.size(); | |||
if (issueCount == 1) { | |||
link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See the single issue")); | |||
} else if (issueCount <= MAX_ISSUES_BY_LINK) { | |||
link(sb, s -> issuePageHref.accept(s, sortedIssues), s -> s.append("See all ").append(issueCount).append(" issues")); | |||
} else { | |||
sb.append("See issues"); | |||
List<List<ChangedIssue>> issueGroups = Lists.partition(ImmutableList.copyOf(sortedIssues), MAX_ISSUES_BY_LINK); | |||
Iterator<List<ChangedIssue>> issueGroupsIterator = issueGroups.iterator(); | |||
int[] groupIndex = new int[] {0}; | |||
while (issueGroupsIterator.hasNext()) { | |||
List<ChangedIssue> issueGroup = issueGroupsIterator.next(); | |||
sb.append(' '); | |||
link(sb, s -> issuePageHref.accept(s, issueGroup), issueGroupLabel(sb, groupIndex, issueGroup)); | |||
groupIndex[0]++; | |||
} | |||
} | |||
} | |||
private static void appendField(StringBuilder sb, String name, @Nullable String oldValue, @Nullable String newValue) { | |||
if (oldValue != null || newValue != null) { | |||
sb.append(name).append(": "); | |||
if (newValue != null) { | |||
sb.append(newValue); | |||
BiConsumer<StringBuilder, Collection<ChangedIssue>> projectIssuePageHref(String projectParams) { | |||
return (s, issues) -> { | |||
s.append(settings.getServerBaseURL()).append("/project/issues?").append(projectParams) | |||
.append("&issues="); | |||
Iterator<ChangedIssue> issueIterator = issues.iterator(); | |||
while (issueIterator.hasNext()) { | |||
s.append(urlEncode(issueIterator.next().getKey())); | |||
if (issueIterator.hasNext()) { | |||
s.append(URL_ENCODED_COMMA); | |||
} | |||
} | |||
if (oldValue != null) { | |||
sb.append(" (was ").append(oldValue).append(")"); | |||
if (issues.size() == 1) { | |||
s.append("&open=").append(urlEncode(issues.iterator().next().getKey())); | |||
} | |||
sb.append(NEW_LINE); | |||
} | |||
}; | |||
} | |||
private static void appendFieldWithoutHistory(StringBuilder sb, String name, @Nullable String oldValue, @Nullable String newValue) { | |||
if (oldValue != null || newValue != null) { | |||
sb.append(name); | |||
if (newValue != null) { | |||
sb.append(" changed to "); | |||
sb.append(newValue); | |||
private static Consumer<StringBuilder> issueGroupLabel(StringBuilder sb, int[] groupIndex, List<ChangedIssue> issueGroup) { | |||
return s -> { | |||
int firstIssueNumber = (groupIndex[0] * MAX_ISSUES_BY_LINK) + 1; | |||
if (issueGroup.size() == 1) { | |||
sb.append(firstIssueNumber); | |||
} else { | |||
sb.append(" removed"); | |||
sb.append(firstIssueNumber).append("-").append(firstIssueNumber + issueGroup.size() - 1); | |||
} | |||
sb.append(NEW_LINE); | |||
} | |||
}; | |||
} | |||
private String getUserFullName(@Nullable String login) { | |||
if (login == null) { | |||
return null; | |||
} | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
UserDto userDto = dbClient.userDao().selectByLogin(dbSession, login); | |||
if (userDto == null || !userDto.isActive()) { | |||
// most probably user was deleted | |||
return login; | |||
} | |||
return StringUtils.defaultIfBlank(userDto.getName(), login); | |||
void addFooter(StringBuilder sb, String notificationI18nKey) { | |||
paragraph(sb, s -> s.append(" ")); | |||
paragraph(sb, s -> { | |||
s.append("<small>"); | |||
s.append("You received this email because you are subscribed to ") | |||
.append('"').append(i18n.message(Locale.ENGLISH, notificationI18nKey, notificationI18nKey)).append('"') | |||
.append(" notifications from ").append(settings.getInstanceName()).append("."); | |||
s.append(" Click "); | |||
link(s, s1 -> s1.append(settings.getServerBaseURL()).append("/account/notifications"), s1 -> s1.append("here")); | |||
s.append(" to edit your email preferences."); | |||
s.append("</small>"); | |||
}); | |||
} | |||
protected static void paragraph(StringBuilder sb, Consumer<StringBuilder> content) { | |||
sb.append("<p>"); | |||
content.accept(sb); | |||
sb.append("</p>"); | |||
} | |||
protected static void link(StringBuilder sb, Consumer<StringBuilder> link, Consumer<StringBuilder> content) { | |||
sb.append("<a href=\""); | |||
link.accept(sb); | |||
sb.append("\">"); | |||
content.accept(sb); | |||
sb.append("</a>"); | |||
} | |||
private static String urlEncode(String str) { | |||
try { | |||
return encode(str, "UTF-8"); | |||
} catch (UnsupportedEncodingException e) { | |||
throw new IllegalStateException(e); | |||
} | |||
} | |||
@@ -0,0 +1,32 @@ | |||
/* | |||
* 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 org.sonar.api.notifications.Notification; | |||
public class IssuesChangesNotification extends Notification { | |||
public static final String TYPE = "issues-changes"; | |||
public IssuesChangesNotification() { | |||
super(TYPE); | |||
} | |||
} |
@@ -0,0 +1,461 @@ | |||
/* | |||
* 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.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.api.rule.RuleKey; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static java.util.Objects.requireNonNull; | |||
import static java.util.Optional.ofNullable; | |||
@Immutable | |||
public class IssuesChangesNotificationBuilder { | |||
private static final String KEY_CANT_BE_NULL_MESSAGE = "key can't be null"; | |||
private final Set<ChangedIssue> issues; | |||
private final Change change; | |||
public IssuesChangesNotificationBuilder(Set<ChangedIssue> issues, Change change) { | |||
checkArgument(!issues.isEmpty(), "issues can't be empty"); | |||
this.issues = ImmutableSet.copyOf(issues); | |||
this.change = requireNonNull(change, "change can't be null"); | |||
} | |||
public Set<ChangedIssue> getIssues() { | |||
return issues; | |||
} | |||
public Change getChange() { | |||
return change; | |||
} | |||
@Immutable | |||
public static final class ChangedIssue { | |||
private final String key; | |||
private final String newStatus; | |||
@CheckForNull | |||
private final String newResolution; | |||
@CheckForNull | |||
private final User assignee; | |||
private final Rule rule; | |||
private final Project project; | |||
public ChangedIssue(Builder builder) { | |||
this.key = requireNonNull(builder.key, KEY_CANT_BE_NULL_MESSAGE); | |||
this.newStatus = requireNonNull(builder.newStatus, "newStatus can't be null"); | |||
this.newResolution = builder.newResolution; | |||
this.assignee = builder.assignee; | |||
this.rule = requireNonNull(builder.rule, "rule can't be null"); | |||
this.project = requireNonNull(builder.project, "project can't be null"); | |||
} | |||
public String getKey() { | |||
return key; | |||
} | |||
public String getNewStatus() { | |||
return newStatus; | |||
} | |||
public Optional<String> getNewResolution() { | |||
return ofNullable(newResolution); | |||
} | |||
public Optional<User> getAssignee() { | |||
return ofNullable(assignee); | |||
} | |||
public Rule getRule() { | |||
return rule; | |||
} | |||
public Project getProject() { | |||
return project; | |||
} | |||
public static class Builder { | |||
private final String key; | |||
private String newStatus; | |||
@CheckForNull | |||
private String newResolution; | |||
@CheckForNull | |||
private User assignee; | |||
private Rule rule; | |||
private Project project; | |||
public Builder(String key) { | |||
this.key = key; | |||
} | |||
public Builder setNewStatus(String newStatus) { | |||
this.newStatus = newStatus; | |||
return this; | |||
} | |||
public Builder setNewResolution(@Nullable String newResolution) { | |||
this.newResolution = newResolution; | |||
return this; | |||
} | |||
public Builder setAssignee(@Nullable User assignee) { | |||
this.assignee = assignee; | |||
return this; | |||
} | |||
public Builder setRule(Rule rule) { | |||
this.rule = rule; | |||
return this; | |||
} | |||
public Builder setProject(Project project) { | |||
this.project = project; | |||
return this; | |||
} | |||
public ChangedIssue build() { | |||
return new ChangedIssue(this); | |||
} | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
ChangedIssue that = (ChangedIssue) o; | |||
return key.equals(that.key) && | |||
newStatus.equals(that.newStatus) && | |||
Objects.equals(newResolution, that.newResolution) && | |||
Objects.equals(assignee, that.assignee) && | |||
rule.equals(that.rule) && | |||
project.equals(that.project); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(key, newStatus, newResolution, assignee, rule, project); | |||
} | |||
@Override | |||
public String toString() { | |||
return "ChangedIssue{" + | |||
"key='" + key + '\'' + | |||
", newStatus='" + newStatus + '\'' + | |||
", newResolution='" + newResolution + '\'' + | |||
", assignee=" + assignee + | |||
", rule=" + rule + | |||
", project=" + project + | |||
'}'; | |||
} | |||
} | |||
public static final class User { | |||
private final String uuid; | |||
private final String login; | |||
@CheckForNull | |||
private final String name; | |||
public User(String uuid, String login, @Nullable String name) { | |||
this.uuid = requireNonNull(uuid, "uuid can't be null"); | |||
this.login = requireNonNull(login, "login can't be null"); | |||
this.name = name; | |||
} | |||
public String getUuid() { | |||
return uuid; | |||
} | |||
public String getLogin() { | |||
return login; | |||
} | |||
public Optional<String> getName() { | |||
return ofNullable(name); | |||
} | |||
@Override | |||
public String toString() { | |||
return "User{" + | |||
"uuid='" + uuid + '\'' + | |||
", login='" + login + '\'' + | |||
", name='" + name + '\'' + | |||
'}'; | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
User user = (User) o; | |||
return uuid.equals(user.uuid) && | |||
login.equals(user.login) && | |||
Objects.equals(name, user.name); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(uuid, login, name); | |||
} | |||
} | |||
@Immutable | |||
public static final class Rule { | |||
private final RuleKey key; | |||
private final String name; | |||
public Rule(RuleKey key, String name) { | |||
this.key = requireNonNull(key, KEY_CANT_BE_NULL_MESSAGE); | |||
this.name = requireNonNull(name, "name can't be null"); | |||
} | |||
public RuleKey getKey() { | |||
return key; | |||
} | |||
public String getName() { | |||
return name; | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
Rule that = (Rule) o; | |||
return key.equals(that.key) && name.equals(that.name); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(key, name); | |||
} | |||
@Override | |||
public String toString() { | |||
return "Rule{" + | |||
"key=" + key + | |||
", name='" + name + '\'' + | |||
'}'; | |||
} | |||
} | |||
@Immutable | |||
public static final class Project { | |||
private final String uuid; | |||
private final String key; | |||
private final String projectName; | |||
@Nullable | |||
private final String branchName; | |||
public Project(Builder builder) { | |||
this.uuid = requireNonNull(builder.uuid, "uuid can't be null"); | |||
this.key = requireNonNull(builder.key, KEY_CANT_BE_NULL_MESSAGE); | |||
this.projectName = requireNonNull(builder.projectName, "projectName can't be null"); | |||
this.branchName = builder.branchName; | |||
} | |||
public String getUuid() { | |||
return uuid; | |||
} | |||
public String getKey() { | |||
return key; | |||
} | |||
public String getProjectName() { | |||
return projectName; | |||
} | |||
public Optional<String> getBranchName() { | |||
return ofNullable(branchName); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
Project project = (Project) o; | |||
return uuid.equals(project.uuid) && | |||
key.equals(project.key) && | |||
projectName.equals(project.projectName) && | |||
Objects.equals(branchName, project.branchName); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(uuid, key, projectName, branchName); | |||
} | |||
@Override | |||
public String toString() { | |||
return "Project{" + | |||
"uuid='" + uuid + '\'' + | |||
", key='" + key + '\'' + | |||
", projectName='" + projectName + '\'' + | |||
", branchName='" + branchName + '\'' + | |||
'}'; | |||
} | |||
public static class Builder { | |||
private final String uuid; | |||
private String key; | |||
private String projectName; | |||
@CheckForNull | |||
private String branchName; | |||
public Builder(String uuid) { | |||
this.uuid = uuid; | |||
} | |||
public Builder setKey(String key) { | |||
this.key = key; | |||
return this; | |||
} | |||
public Builder setProjectName(String projectName) { | |||
this.projectName = projectName; | |||
return this; | |||
} | |||
public Builder setBranchName(@Nullable String branchName) { | |||
this.branchName = branchName; | |||
return this; | |||
} | |||
public Project build() { | |||
return new Project(this); | |||
} | |||
} | |||
} | |||
public abstract static class Change { | |||
protected final long date; | |||
private Change(long date) { | |||
this.date = requireNonNull(date, "date can't be null"); | |||
} | |||
public long getDate() { | |||
return date; | |||
} | |||
public abstract boolean isAuthorLogin(String login); | |||
} | |||
@Immutable | |||
public static final class AnalysisChange extends Change { | |||
public AnalysisChange(long date) { | |||
super(date); | |||
} | |||
@Override | |||
public boolean isAuthorLogin(String login) { | |||
return false; | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
Change change = (Change) o; | |||
return date == change.date; | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(date); | |||
} | |||
@Override | |||
public String toString() { | |||
return "AnalysisChange{" + date + '}'; | |||
} | |||
} | |||
@Immutable | |||
public static final class UserChange extends Change { | |||
private final User user; | |||
public UserChange(long date, User user) { | |||
super(date); | |||
this.user = requireNonNull(user, "user can't be null"); | |||
} | |||
public User getUser() { | |||
return user; | |||
} | |||
@Override | |||
public boolean isAuthorLogin(String login) { | |||
return this.user.login.equals(login); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
UserChange that = (UserChange) o; | |||
return date == that.date && user.equals(that.user); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(user, date); | |||
} | |||
@Override | |||
public String toString() { | |||
return "UserChange{" + | |||
"date=" + date + | |||
", user=" + user + | |||
'}'; | |||
} | |||
} | |||
} |
@@ -0,0 +1,37 @@ | |||
/* | |||
* 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 org.sonar.core.platform.Module; | |||
public class IssuesChangesNotificationModule extends Module { | |||
@Override | |||
protected void configureModule() { | |||
add( | |||
ChangesOnMyIssueNotificationHandler.class, | |||
ChangesOnMyIssueNotificationHandler.newMetadata(), | |||
ChangesOnMyIssuesEmailTemplate.class, | |||
FPOrWontFixNotificationHandler.class, | |||
FPOrWontFixNotificationHandler.newMetadata(), | |||
IssuesChangesNotificationSerializer.class, | |||
FpOrWontFixEmailTemplate.class | |||
); | |||
} | |||
} |
@@ -0,0 +1,307 @@ | |||
/* | |||
* 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 java.util.ArrayList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
public class IssuesChangesNotificationSerializer { | |||
private static final String FIELD_ISSUES_COUNT = "issues.count"; | |||
private static final String FIELD_CHANGE_DATE = "change.date"; | |||
private static final String FIELD_CHANGE_AUTHOR_UUID = "change.author.uuid"; | |||
private static final String FIELD_CHANGE_AUTHOR_LOGIN = "change.author.login"; | |||
private static final String FIELD_CHANGE_AUTHOR_NAME = "change.author.name"; | |||
public IssuesChangesNotification serialize(IssuesChangesNotificationBuilder builder) { | |||
IssuesChangesNotification res = new IssuesChangesNotification(); | |||
serializeIssueSize(res, builder.getIssues()); | |||
serializeChange(res, builder.getChange()); | |||
serializeIssues(res, builder.getIssues()); | |||
serializeRules(res, builder.getIssues()); | |||
serializeProjects(res, builder.getIssues()); | |||
return res; | |||
} | |||
/** | |||
* @throws IllegalArgumentException if {@code notification} misses any field or of any has unsupported value | |||
*/ | |||
public IssuesChangesNotificationBuilder from(IssuesChangesNotification notification) { | |||
int issueCount = readIssueCount(notification); | |||
IssuesChangesNotificationBuilder.Change change = readChange(notification); | |||
List<Issue> issues = readIssues(notification, issueCount); | |||
Map<String, Project> projects = readProjects(notification, issues); | |||
Map<RuleKey, Rule> rules = readRules(notification, issues); | |||
return new IssuesChangesNotificationBuilder(buildChangedIssues(issues, projects, rules), change); | |||
} | |||
private static void serializeIssueSize(IssuesChangesNotification res, Set<ChangedIssue> issues) { | |||
res.setFieldValue(FIELD_ISSUES_COUNT, String.valueOf(issues.size())); | |||
} | |||
private static int readIssueCount(IssuesChangesNotification notification) { | |||
String fieldValue = notification.getFieldValue(FIELD_ISSUES_COUNT); | |||
checkArgument(fieldValue != null, "missing field %s", FIELD_ISSUES_COUNT); | |||
int issueCount = Integer.parseInt(fieldValue); | |||
checkArgument(issueCount > 0, "issue count must be >= 1"); | |||
return issueCount; | |||
} | |||
private static Set<ChangedIssue> buildChangedIssues(List<Issue> issues, Map<String, Project> projects, | |||
Map<RuleKey, Rule> rules) { | |||
return issues.stream() | |||
.map(issue -> new ChangedIssue.Builder(issue.key) | |||
.setNewStatus(issue.newStatus) | |||
.setNewResolution(issue.newResolution) | |||
.setAssignee(issue.assignee) | |||
.setRule(rules.get(issue.ruleKey)) | |||
.setProject(projects.get(issue.projectUuid)) | |||
.build()) | |||
.collect(toSet(issues.size())); | |||
} | |||
private static void serializeIssues(IssuesChangesNotification res, Set<ChangedIssue> issues) { | |||
int index = 0; | |||
for (ChangedIssue issue : issues) { | |||
serializeIssue(res, index, issue); | |||
index++; | |||
} | |||
} | |||
private static List<Issue> readIssues(IssuesChangesNotification notification, int issueCount) { | |||
List<Issue> res = new ArrayList<>(issueCount); | |||
for (int i = 0; i < issueCount; i++) { | |||
res.add(readIssue(notification, i)); | |||
} | |||
return res; | |||
} | |||
private static void serializeIssue(IssuesChangesNotification notification, int index, ChangedIssue issue) { | |||
String issuePropertyPrefix = "issues." + index; | |||
notification.setFieldValue(issuePropertyPrefix + ".key", issue.getKey()); | |||
issue.getAssignee() | |||
.ifPresent(assignee -> { | |||
notification.setFieldValue(issuePropertyPrefix + ".assignee.uuid", assignee.getUuid()); | |||
notification.setFieldValue(issuePropertyPrefix + ".assignee.login", assignee.getLogin()); | |||
assignee.getName() | |||
.ifPresent(name -> notification.setFieldValue(issuePropertyPrefix + ".assignee.name", name)); | |||
}); | |||
issue.getNewResolution() | |||
.ifPresent(newResolution -> notification.setFieldValue(issuePropertyPrefix + ".newResolution", newResolution)); | |||
notification.setFieldValue(issuePropertyPrefix + ".newStatus", issue.getNewStatus()); | |||
notification.setFieldValue(issuePropertyPrefix + ".ruleKey", issue.getRule().getKey().toString()); | |||
notification.setFieldValue(issuePropertyPrefix + ".projectUuid", issue.getProject().getUuid()); | |||
} | |||
private static Issue readIssue(IssuesChangesNotification notification, int index) { | |||
String issuePropertyPrefix = "issues." + index; | |||
User assignee = readAssignee(notification, issuePropertyPrefix, index); | |||
return new Issue.Builder() | |||
.setKey(getIssueFieldValue(notification, issuePropertyPrefix + ".key", index)) | |||
.setNewStatus(getIssueFieldValue(notification, issuePropertyPrefix + ".newStatus", index)) | |||
.setNewResolution(notification.getFieldValue(issuePropertyPrefix + ".newResolution")) | |||
.setAssignee(assignee) | |||
.setRuleKey(getIssueFieldValue(notification, issuePropertyPrefix + ".ruleKey", index)) | |||
.setProjectUuid(getIssueFieldValue(notification, issuePropertyPrefix + ".projectUuid", index)) | |||
.build(); | |||
} | |||
@CheckForNull | |||
private static User readAssignee(IssuesChangesNotification notification, String issuePropertyPrefix, int index) { | |||
String uuid = notification.getFieldValue(issuePropertyPrefix + ".assignee.uuid"); | |||
if (uuid == null) { | |||
return null; | |||
} | |||
String login = getIssueFieldValue(notification, issuePropertyPrefix + ".assignee.login", index); | |||
return new User(uuid, login, notification.getFieldValue(issuePropertyPrefix + ".assignee.name")); | |||
} | |||
private static String getIssueFieldValue(IssuesChangesNotification notification, String fieldName, int index) { | |||
String fieldValue = notification.getFieldValue(fieldName); | |||
checkState(fieldValue != null, "Can not find field %s for issue with index %s", fieldName, index); | |||
return fieldValue; | |||
} | |||
private static void serializeRules(IssuesChangesNotification res, Set<ChangedIssue> issues) { | |||
issues.stream() | |||
.map(ChangedIssue::getRule) | |||
.collect(Collectors.toSet()) | |||
.forEach(rule -> res.setFieldValue("rules." + rule.getKey(), rule.getName())); | |||
} | |||
private static Map<RuleKey, Rule> readRules(IssuesChangesNotification notification, List<Issue> issues) { | |||
return issues.stream() | |||
.map(issue -> issue.ruleKey) | |||
.collect(Collectors.toSet()) | |||
.stream() | |||
.map(ruleKey -> readRule(notification, ruleKey)) | |||
.collect(uniqueIndex(Rule::getKey, t -> t)); | |||
} | |||
private static Rule readRule(IssuesChangesNotification notification, RuleKey ruleKey) { | |||
String fieldName = "rules." + ruleKey; | |||
String ruleName = notification.getFieldValue(fieldName); | |||
checkState(ruleName != null, "can not find field %s", ruleKey); | |||
return new Rule(ruleKey, ruleName); | |||
} | |||
private static void serializeProjects(IssuesChangesNotification res, Set<ChangedIssue> issues) { | |||
issues.stream() | |||
.map(ChangedIssue::getProject) | |||
.collect(Collectors.toSet()) | |||
.forEach(project -> { | |||
String projectPropertyPrefix = "projects." + project.getUuid(); | |||
res.setFieldValue(projectPropertyPrefix + ".key", project.getKey()); | |||
res.setFieldValue(projectPropertyPrefix + ".projectName", project.getProjectName()); | |||
project.getBranchName() | |||
.ifPresent(branchName -> res.setFieldValue(projectPropertyPrefix + ".branchName", branchName)); | |||
}); | |||
} | |||
private static Map<String, Project> readProjects(IssuesChangesNotification notification, List<Issue> issues) { | |||
return issues.stream() | |||
.map(issue -> issue.projectUuid) | |||
.collect(Collectors.toSet()) | |||
.stream() | |||
.map(projectUuid -> { | |||
String projectPropertyPrefix = "projects." + projectUuid; | |||
return new Project.Builder(projectUuid) | |||
.setKey(getProjectFieldValue(notification, projectPropertyPrefix + ".key", projectUuid)) | |||
.setProjectName(getProjectFieldValue(notification, projectPropertyPrefix + ".projectName", projectUuid)) | |||
.setBranchName(notification.getFieldValue(projectPropertyPrefix + ".branchName")) | |||
.build(); | |||
}) | |||
.collect(uniqueIndex(Project::getUuid, t -> t)); | |||
} | |||
private static String getProjectFieldValue(IssuesChangesNotification notification, String fieldName, String uuid) { | |||
String fieldValue = notification.getFieldValue(fieldName); | |||
checkState(fieldValue != null, "Can not find field %s for project with uuid %s", fieldName, uuid); | |||
return fieldValue; | |||
} | |||
private static void serializeChange(IssuesChangesNotification notification, IssuesChangesNotificationBuilder.Change change) { | |||
notification.setFieldValue(FIELD_CHANGE_DATE, String.valueOf(change.date)); | |||
if (change instanceof IssuesChangesNotificationBuilder.UserChange) { | |||
IssuesChangesNotificationBuilder.UserChange userChange = (IssuesChangesNotificationBuilder.UserChange) change; | |||
User user = userChange.getUser(); | |||
notification.setFieldValue(FIELD_CHANGE_AUTHOR_UUID, user.getUuid()); | |||
notification.setFieldValue(FIELD_CHANGE_AUTHOR_LOGIN, user.getLogin()); | |||
user.getName().ifPresent(name -> notification.setFieldValue(FIELD_CHANGE_AUTHOR_NAME, name)); | |||
} | |||
} | |||
private static IssuesChangesNotificationBuilder.Change readChange(IssuesChangesNotification notification) { | |||
String dateFieldValue = notification.getFieldValue(FIELD_CHANGE_DATE); | |||
checkState(dateFieldValue != null, "Can not find field %s", FIELD_CHANGE_DATE); | |||
long date = Long.parseLong(dateFieldValue); | |||
String uuid = notification.getFieldValue(FIELD_CHANGE_AUTHOR_UUID); | |||
if (uuid == null) { | |||
return new IssuesChangesNotificationBuilder.AnalysisChange(date); | |||
} | |||
String login = notification.getFieldValue(FIELD_CHANGE_AUTHOR_LOGIN); | |||
checkState(login != null, "Can not find field %s", FIELD_CHANGE_AUTHOR_LOGIN); | |||
return new IssuesChangesNotificationBuilder.UserChange(date, new User(uuid, login, notification.getFieldValue(FIELD_CHANGE_AUTHOR_NAME))); | |||
} | |||
@Immutable | |||
private static final class Issue { | |||
private final String key; | |||
private final String newStatus; | |||
@CheckForNull | |||
private final String newResolution; | |||
@CheckForNull | |||
private final User assignee; | |||
private final RuleKey ruleKey; | |||
private final String projectUuid; | |||
private Issue(Builder builder) { | |||
this.key = builder.key; | |||
this.newResolution = builder.newResolution; | |||
this.newStatus = builder.newStatus; | |||
this.assignee = builder.assignee; | |||
this.ruleKey = RuleKey.parse(builder.ruleKey); | |||
this.projectUuid = builder.projectUuid; | |||
} | |||
static class Builder { | |||
private String key = null; | |||
private String newStatus = null; | |||
@CheckForNull | |||
private String newResolution = null; | |||
@CheckForNull | |||
private User assignee = null; | |||
private String ruleKey = null; | |||
private String projectUuid = null; | |||
public Builder setKey(String key) { | |||
this.key = key; | |||
return this; | |||
} | |||
public Builder setNewStatus(String newStatus) { | |||
this.newStatus = newStatus; | |||
return this; | |||
} | |||
public Builder setNewResolution(@Nullable String newResolution) { | |||
this.newResolution = newResolution; | |||
return this; | |||
} | |||
public Builder setAssignee(@Nullable User assignee) { | |||
this.assignee = assignee; | |||
return this; | |||
} | |||
public Builder setRuleKey(String ruleKey) { | |||
this.ruleKey = ruleKey; | |||
return this; | |||
} | |||
public Builder setProjectUuid(String projectUuid) { | |||
this.projectUuid = projectUuid; | |||
return this; | |||
} | |||
public Issue build() { | |||
return new Issue(this); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,47 @@ | |||
/* | |||
* 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 java.util.Set; | |||
import java.util.stream.Collectors; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
final class NotificationWithProjectKeys { | |||
private final IssuesChangesNotificationBuilder builder; | |||
private final Set<String> projectKeys; | |||
protected NotificationWithProjectKeys(IssuesChangesNotificationBuilder builder) { | |||
this.builder = builder; | |||
this.projectKeys = builder.getIssues().stream().map(t -> t.getProject().getKey()).collect(Collectors.toSet()); | |||
} | |||
public Set<ChangedIssue> getIssues() { | |||
return builder.getIssues(); | |||
} | |||
public Change getChange() { | |||
return builder.getChange(); | |||
} | |||
public Set<String> getProjectKeys() { | |||
return projectKeys; | |||
} | |||
} |
@@ -26,7 +26,9 @@ import java.util.Set; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.apache.commons.mail.Email; | |||
import org.apache.commons.mail.EmailException; | |||
import org.apache.commons.mail.HtmlEmail; | |||
import org.apache.commons.mail.SimpleEmail; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.notifications.Notification; | |||
@@ -227,46 +229,13 @@ public class EmailNotificationChannel extends NotificationChannel { | |||
try { | |||
LOG.trace("Sending email: {}", emailMessage); | |||
String host = null; | |||
try { | |||
host = new URL(configuration.getServerBaseURL()).getHost(); | |||
} catch (MalformedURLException e) { | |||
// ignore | |||
} | |||
String host = resolveHost(); | |||
SimpleEmail email = new SimpleEmail(); | |||
if (StringUtils.isNotBlank(host)) { | |||
/* | |||
* Set headers for proper threading: GMail will not group messages, even if they have same subject, but don't have "In-Reply-To" and | |||
* "References" headers. TODO investigate threading in other clients like KMail, Thunderbird, Outlook | |||
*/ | |||
if (StringUtils.isNotEmpty(emailMessage.getMessageId())) { | |||
String messageId = "<" + emailMessage.getMessageId() + "@" + host + ">"; | |||
email.addHeader(IN_REPLY_TO_HEADER, messageId); | |||
email.addHeader(REFERENCES_HEADER, messageId); | |||
} | |||
// Set headers for proper filtering | |||
email.addHeader(LIST_ID_HEADER, "SonarQube <sonar." + host + ">"); | |||
email.addHeader(LIST_ARCHIVE_HEADER, configuration.getServerBaseURL()); | |||
} | |||
// Set general information | |||
email.setCharset("UTF-8"); | |||
String fromName = configuration.getFromName(); | |||
String from = StringUtils.isBlank(emailMessage.getFrom()) ? fromName : (emailMessage.getFrom() + " (" + fromName + ")"); | |||
email.setFrom(configuration.getFrom(), from); | |||
email.addTo(emailMessage.getTo(), " "); | |||
String subject = StringUtils.defaultIfBlank(StringUtils.trimToEmpty(configuration.getPrefix()) + " ", "") | |||
+ StringUtils.defaultString(emailMessage.getSubject(), SUBJECT_DEFAULT); | |||
email.setSubject(subject); | |||
email.setMsg(emailMessage.getMessage()); | |||
// Send | |||
email.setHostName(configuration.getSmtpHost()); | |||
configureSecureConnection(email); | |||
if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) { | |||
email.setAuthentication(configuration.getSmtpUsername(), configuration.getSmtpPassword()); | |||
} | |||
email.setSocketConnectionTimeout(SOCKET_TIMEOUT); | |||
email.setSocketTimeout(SOCKET_TIMEOUT); | |||
Email email = createEmailWithMessage(emailMessage); | |||
setHeaders(email, emailMessage, host); | |||
setConnectionDetails(email); | |||
setToAndFrom(email, emailMessage); | |||
setSubject(email, emailMessage); | |||
email.send(); | |||
} finally { | |||
@@ -274,7 +243,66 @@ public class EmailNotificationChannel extends NotificationChannel { | |||
} | |||
} | |||
private void configureSecureConnection(SimpleEmail email) { | |||
private static Email createEmailWithMessage(EmailMessage emailMessage) throws EmailException { | |||
if (emailMessage.isHtml()) { | |||
return new HtmlEmail().setHtmlMsg(emailMessage.getMessage()); | |||
} | |||
return new SimpleEmail().setMsg(emailMessage.getMessage()); | |||
} | |||
private void setSubject(Email email, EmailMessage emailMessage) { | |||
String subject = StringUtils.defaultIfBlank(StringUtils.trimToEmpty(configuration.getPrefix()) + " ", "") | |||
+ StringUtils.defaultString(emailMessage.getSubject(), SUBJECT_DEFAULT); | |||
email.setSubject(subject); | |||
} | |||
private void setToAndFrom(Email email, EmailMessage emailMessage) throws EmailException { | |||
String fromName = configuration.getFromName(); | |||
String from = StringUtils.isBlank(emailMessage.getFrom()) ? fromName : (emailMessage.getFrom() + " (" + fromName + ")"); | |||
email.setFrom(configuration.getFrom(), from); | |||
email.addTo(emailMessage.getTo(), " "); | |||
} | |||
@CheckForNull | |||
private String resolveHost() { | |||
try { | |||
return new URL(configuration.getServerBaseURL()).getHost(); | |||
} catch (MalformedURLException e) { | |||
// ignore | |||
return null; | |||
} | |||
} | |||
private void setHeaders(Email email, EmailMessage emailMessage, @CheckForNull String host) { | |||
// Set general information | |||
email.setCharset("UTF-8"); | |||
if (StringUtils.isNotBlank(host)) { | |||
/* | |||
* Set headers for proper threading: GMail will not group messages, even if they have same subject, but don't have "In-Reply-To" and | |||
* "References" headers. TODO investigate threading in other clients like KMail, Thunderbird, Outlook | |||
*/ | |||
if (StringUtils.isNotEmpty(emailMessage.getMessageId())) { | |||
String messageId = "<" + emailMessage.getMessageId() + "@" + host + ">"; | |||
email.addHeader(IN_REPLY_TO_HEADER, messageId); | |||
email.addHeader(REFERENCES_HEADER, messageId); | |||
} | |||
// Set headers for proper filtering | |||
email.addHeader(LIST_ID_HEADER, "SonarQube <sonar." + host + ">"); | |||
email.addHeader(LIST_ARCHIVE_HEADER, configuration.getServerBaseURL()); | |||
} | |||
} | |||
private void setConnectionDetails(Email email) { | |||
email.setHostName(configuration.getSmtpHost()); | |||
configureSecureConnection(email); | |||
if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) { | |||
email.setAuthentication(configuration.getSmtpUsername(), configuration.getSmtpPassword()); | |||
} | |||
email.setSocketConnectionTimeout(SOCKET_TIMEOUT); | |||
email.setSocketTimeout(SOCKET_TIMEOUT); | |||
} | |||
private void configureSecureConnection(Email email) { | |||
if (StringUtils.equalsIgnoreCase(configuration.getSecureConnection(), "ssl")) { | |||
email.setSSLOnConnect(true); | |||
email.setSSLCheckServerIdentity(true); | |||
@@ -305,7 +333,7 @@ public class EmailNotificationChannel extends NotificationChannel { | |||
EmailMessage emailMessage = new EmailMessage(); | |||
emailMessage.setTo(toAddress); | |||
emailMessage.setSubject(subject); | |||
emailMessage.setMessage(message); | |||
emailMessage.setPlainTextMessage(message); | |||
send(emailMessage); | |||
} catch (EmailException e) { | |||
LOG.debug("Fail to send test email to {}: {}", toAddress, e); |
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.server.qualitygate.notification; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.sonar.api.config.EmailSettings; | |||
@@ -41,6 +42,7 @@ public class QGChangeEmailTemplate implements EmailTemplate { | |||
} | |||
@Override | |||
@CheckForNull | |||
public EmailMessage format(Notification notification) { | |||
if (!"alerts".equals(notification.getType())) { | |||
return null; | |||
@@ -66,7 +68,7 @@ public class QGChangeEmailTemplate implements EmailTemplate { | |||
return new EmailMessage() | |||
.setMessageId("alerts/" + projectId) | |||
.setSubject(subject) | |||
.setMessage(messageBody); | |||
.setPlainTextMessage(messageBody); | |||
} | |||
private static String computeFullProjectName(String projectName, @Nullable String branchName) { |
@@ -20,10 +20,12 @@ | |||
package org.sonar.server.issue.notification; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.google.common.collect.ListMultimap; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.Random; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
@@ -32,22 +34,33 @@ import java.util.stream.Stream; | |||
import javax.annotation.Nullable; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.mockito.ArgumentCaptor; | |||
import org.mockito.Mockito; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import org.sonar.server.notification.NotificationDispatcherMetadata; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.notification.email.EmailNotificationChannel; | |||
import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest; | |||
import static java.util.Collections.singleton; | |||
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.ArgumentMatchers.anySet; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.times; | |||
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.core.util.stream.MoreCollectors.index; | |||
import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex; | |||
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; | |||
@@ -59,7 +72,12 @@ public class ChangesOnMyIssueNotificationHandlerTest { | |||
private NotificationManager notificationManager = mock(NotificationManager.class); | |||
private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class); | |||
private ChangesOnMyIssueNotificationHandler underTest = new ChangesOnMyIssueNotificationHandler(notificationManager, emailNotificationChannel); | |||
private IssuesChangesNotificationSerializer serializer = new IssuesChangesNotificationSerializer(); | |||
private ChangesOnMyIssueNotificationHandler underTest = new ChangesOnMyIssueNotificationHandler( | |||
notificationManager, emailNotificationChannel, serializer); | |||
private Class<Set<EmailDeliveryRequest>> emailDeliveryRequestSetType = (Class<Set<EmailDeliveryRequest>>) (Object) Set.class; | |||
private ArgumentCaptor<Set<EmailDeliveryRequest>> emailDeliveryRequestSetCaptor = ArgumentCaptor.forClass(emailDeliveryRequestSetType); | |||
@Test | |||
public void getMetadata_returns_same_instance_as_static_method() { | |||
@@ -89,7 +107,7 @@ public class ChangesOnMyIssueNotificationHandlerTest { | |||
@Test | |||
public void getNotificationClass_is_IssueChangeNotification() { | |||
assertThat(underTest.getNotificationClass()).isEqualTo(IssueChangeNotification.class); | |||
assertThat(underTest.getNotificationClass()).isEqualTo(IssuesChangesNotification.class); | |||
} | |||
@Test | |||
@@ -104,8 +122,8 @@ public class ChangesOnMyIssueNotificationHandlerTest { | |||
@Test | |||
public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(false); | |||
Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> mock(IssueChangeNotification.class)) | |||
Set<IssuesChangesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> mock(IssuesChangesNotification.class)) | |||
.collect(toSet()); | |||
int deliver = underTest.deliver(notifications); | |||
@@ -118,29 +136,47 @@ public class ChangesOnMyIssueNotificationHandlerTest { | |||
} | |||
@Test | |||
public void deliver_has_no_effect_if_no_notification_has_projectKey() { | |||
public void deliver_has_no_effect_if_no_notification_has_assignee() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> newNotification(null, null, null)) | |||
Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i) | |||
.setNewStatus("foo") | |||
.setAssignee(null) | |||
.setRule(newRule()) | |||
.setProject(newProject(i + "")) | |||
.build()) | |||
.collect(toSet()); | |||
IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder(issues, new UserChange(new Random().nextLong(), new User("user_uuid", "user_login", null))); | |||
int deliver = underTest.deliver(notifications); | |||
int deliver = underTest.deliver(ImmutableSet.of(serializer.serialize(builder))); | |||
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() { | |||
public void deliver_has_no_effect_if_all_issues_are_assigned_to_the_changeAuthor() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> newNotification(randomAlphabetic(5 + i), null, NO_CHANGE_AUTHOR)) | |||
Set<UserChange> userChanges = IntStream.range(0, 1 + new Random().nextInt(3)) | |||
.mapToObj(i -> new UserChange(new Random().nextLong(), new User("user_uuid_" + i, "user_login_" + i, null))) | |||
.collect(toSet()); | |||
Set<IssuesChangesNotificationBuilder> notificationBuilders = userChanges.stream() | |||
.map(userChange -> { | |||
Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i + userChange.getUser().getUuid()) | |||
.setNewStatus("foo") | |||
.setAssignee(userChange.getUser()) | |||
.setRule(newRule()) | |||
.setProject(newProject(i + "")) | |||
.build()) | |||
.collect(toSet()); | |||
return new IssuesChangesNotificationBuilder(issues, userChange); | |||
}) | |||
.collect(toSet()); | |||
Set<IssuesChangesNotification> notifications = notificationBuilders.stream() | |||
.map(t -> serializer.serialize(t)) | |||
.collect(toSet()); | |||
int deliver = underTest.deliver(notifications); | |||
@@ -149,150 +185,269 @@ public class ChangesOnMyIssueNotificationHandlerTest { | |||
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_change_author_different_from_assignee() { | |||
public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_ChangesOnMyIssues_notifications() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> { | |||
String assignee = randomAlphabetic(4 + i); | |||
return newNotification(randomAlphabetic(5 + i), assignee, assignee); | |||
}) | |||
Project project = newProject(); | |||
Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i) | |||
.setNewStatus("foo") | |||
.setAssignee(newUser("assignee_" + i)) | |||
.setRule(newRule()) | |||
.setProject(project) | |||
.build()) | |||
.collect(toSet()); | |||
IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder(issues, new UserChange(new Random().nextLong(), new User("user_uuid", "user_login", null))); | |||
int deliver = underTest.deliver(notifications); | |||
int deliver = underTest.deliver(ImmutableSet.of(serializer.serialize(builder))); | |||
assertThat(deliver).isZero(); | |||
verifyZeroInteractions(notificationManager); | |||
Set<String> assigneeLogins = issues.stream().map(i -> i.getAssignee().get().getLogin()).collect(toSet()); | |||
verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project.getKey(), assigneeLogins, ALL_MUST_HAVE_ROLE_USER); | |||
verifyNoMoreInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
notifications.forEach(notification -> { | |||
verify(notification).getProjectKey(); | |||
verify(notification, times(2)).getAssignee(); | |||
verify(notification).getChangeAuthor(); | |||
verifyNoMoreInteractions(notification); | |||
}); | |||
} | |||
@Test | |||
@UseDataProvider("noOrDifferentChangeAuthor") | |||
public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_ChangesOnMyIssues_notifications(@Nullable String noOrDifferentChangeAuthor) { | |||
String projectKey1 = randomAlphabetic(10); | |||
String assignee1 = randomAlphabetic(11); | |||
String projectKey2 = randomAlphabetic(12); | |||
String assignee2 = randomAlphabetic(13); | |||
Set<IssueChangeNotification> notifications1 = randomSetOfNotifications(projectKey1, assignee1, noOrDifferentChangeAuthor); | |||
Set<IssueChangeNotification> notifications2 = randomSetOfNotifications(projectKey2, assignee2, noOrDifferentChangeAuthor); | |||
public void deliver_checks_by_projectKeys_if_notifications_have_subscribed_assignee_to_ChangesOnMyIssues_notifications() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<ChangedIssue> issues = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(i -> new ChangedIssue.Builder("issue_key_" + i) | |||
.setNewStatus("foo") | |||
.setAssignee(newUser("" + i)) | |||
.setRule(newRule()) | |||
.setProject(newProject(i + "")) | |||
.build()) | |||
.collect(toSet()); | |||
IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder(issues, new UserChange(new Random().nextLong(), new User("user_uuid", "user_login", null))); | |||
int deliver = underTest.deliver(Stream.concat(notifications1.stream(), notifications2.stream()).collect(toSet())); | |||
int deliver = underTest.deliver(ImmutableSet.of(serializer.serialize(builder))); | |||
assertThat(deliver).isZero(); | |||
verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey1, singleton(assignee1), ALL_MUST_HAVE_ROLE_USER); | |||
verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey2, singleton(assignee2), ALL_MUST_HAVE_ROLE_USER); | |||
issues.stream() | |||
.collect(MoreCollectors.index(ChangedIssue::getProject)) | |||
.asMap() | |||
.forEach((key, value) -> { | |||
String projectKey = key.getKey(); | |||
Set<String> assigneeLogins = value.stream().map(i -> i.getAssignee().get().getLogin()).collect(toSet()); | |||
verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, assigneeLogins, ALL_MUST_HAVE_ROLE_USER); | |||
}); | |||
verifyNoMoreInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
} | |||
@Test | |||
@UseDataProvider("noOrDifferentChangeAuthor") | |||
public void deliver_ignores_notifications_which_assignee_has_not_subscribed_to_ChangesOnMyIssues_notifications(@Nullable String noOrDifferentChangeAuthor) { | |||
String projectKey = randomAlphabetic(5); | |||
String assignee1 = randomAlphabetic(6); | |||
String assignee2 = randomAlphabetic(7); | |||
// assignee1 is not authorized | |||
Set<IssueChangeNotification> assignee1Notifications = randomSetOfNotifications(projectKey, assignee1, noOrDifferentChangeAuthor); | |||
// assignee2 is authorized | |||
Set<IssueChangeNotification> assignee2Notifications = randomSetOfNotifications(projectKey, assignee2, noOrDifferentChangeAuthor); | |||
@UseDataProvider("userOrAnalysisChange") | |||
public void deliver_creates_a_notification_per_assignee_with_only_his_issues_on_the_single_project(Change userOrAnalysisChange) { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
when(notificationManager.findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, ImmutableSet.of(assignee1, assignee2), ALL_MUST_HAVE_ROLE_USER)) | |||
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee2))); | |||
Set<EmailDeliveryRequest> expectedRequests = assignee2Notifications.stream() | |||
.map(t -> new EmailDeliveryRequest(emailOf(t.getAssignee()), t)) | |||
Project project = newProject(); | |||
User assignee1 = newUser("assignee_1"); | |||
User assignee2 = newUser("assignee_2"); | |||
Set<ChangedIssue> assignee1Issues = IntStream.range(0, 10) | |||
.mapToObj(i -> newChangedIssue("1_issue_key_" + i, assignee1, project)) | |||
.collect(toSet()); | |||
int deliveredCount = new Random().nextInt(expectedRequests.size()); | |||
when(emailNotificationChannel.deliverAll(expectedRequests)).thenReturn(deliveredCount); | |||
Set<ChangedIssue> assignee2Issues = IntStream.range(0, 10) | |||
.mapToObj(i -> newChangedIssue("2_issue_key_" + i, assignee2, project)) | |||
.collect(toSet()); | |||
Set<IssuesChangesNotification> notifications = Stream.of( | |||
// notification with only assignee1 5 notifications | |||
new IssuesChangesNotificationBuilder(assignee1Issues.stream().limit(5).collect(toSet()), userOrAnalysisChange), | |||
// notification with only assignee2 6 notifications | |||
new IssuesChangesNotificationBuilder(assignee2Issues.stream().limit(6).collect(toSet()), userOrAnalysisChange), | |||
// notification with 4 assignee1 and 3 assignee2 notifications | |||
new IssuesChangesNotificationBuilder( | |||
Stream.concat(assignee1Issues.stream().skip(6), assignee2Issues.stream().skip(7)).collect(toSet()), | |||
userOrAnalysisChange)) | |||
.map(t -> serializer.serialize(t)) | |||
.collect(toSet()); | |||
when(notificationManager.findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project.getKey(), ImmutableSet.of(assignee1.getLogin(), assignee2.getLogin()), | |||
ALL_MUST_HAVE_ROLE_USER)) | |||
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin()), emailRecipientOf(assignee2.getLogin()))); | |||
int deliveredCount = new Random().nextInt(100); | |||
when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount); | |||
int deliver = underTest.deliver(Stream.concat(assignee1Notifications.stream(), assignee2Notifications.stream()).collect(toSet())); | |||
int deliver = underTest.deliver(notifications); | |||
assertThat(deliver).isEqualTo(deliveredCount); | |||
verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, ImmutableSet.of(assignee1, assignee2), ALL_MUST_HAVE_ROLE_USER); | |||
verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, | |||
project.getKey(), ImmutableSet.of(assignee1.getLogin(), assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER); | |||
verifyNoMoreInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
verify(emailNotificationChannel).deliverAll(expectedRequests); | |||
verify(emailNotificationChannel).deliverAll(emailDeliveryRequestSetCaptor.capture()); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
Set<EmailDeliveryRequest> emailDeliveryRequests = emailDeliveryRequestSetCaptor.getValue(); | |||
assertThat(emailDeliveryRequests).hasSize(4); | |||
ListMultimap<String, EmailDeliveryRequest> emailDeliveryRequestByEmail = emailDeliveryRequests.stream() | |||
.collect(index(EmailDeliveryRequest::getRecipientEmail)); | |||
List<EmailDeliveryRequest> assignee1Requests = emailDeliveryRequestByEmail.get(emailOf(assignee1.getLogin())); | |||
assertThat(assignee1Requests) | |||
.hasSize(2) | |||
.extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification()) | |||
.extracting(ChangesOnMyIssuesNotification::getChange) | |||
.containsOnly(userOrAnalysisChange); | |||
assertThat(assignee1Requests) | |||
.extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification()) | |||
.extracting(ChangesOnMyIssuesNotification::getChangedIssues) | |||
.containsOnly( | |||
assignee1Issues.stream().limit(5).collect(unorderedIndex(t -> project, t -> t)), | |||
assignee1Issues.stream().skip(6).collect(unorderedIndex(t -> project, t -> t))); | |||
List<EmailDeliveryRequest> assignee2Requests = emailDeliveryRequestByEmail.get(emailOf(assignee2.getLogin())); | |||
assertThat(assignee2Requests) | |||
.hasSize(2) | |||
.extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification()) | |||
.extracting(ChangesOnMyIssuesNotification::getChange) | |||
.containsOnly(userOrAnalysisChange); | |||
assertThat(assignee2Requests) | |||
.extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification()) | |||
.extracting(ChangesOnMyIssuesNotification::getChangedIssues) | |||
.containsOnly( | |||
assignee2Issues.stream().limit(6).collect(unorderedIndex(t -> project, t -> t)), | |||
assignee2Issues.stream().skip(7).collect(unorderedIndex(t -> project, t -> t))); | |||
} | |||
@Test | |||
public void deliver_ignores_notifications_which_assignee_is_the_changeAuthor() { | |||
String projectKey = randomAlphabetic(5); | |||
String assignee1 = randomAlphabetic(6); | |||
String assignee2 = randomAlphabetic(7); | |||
String assignee3 = randomAlphabetic(8); | |||
// assignee1 is the changeAuthor of every notification he's the assignee of | |||
Set<IssueChangeNotification> assignee1ChangeAuthor = randomSetOfNotifications(projectKey, assignee1, assignee1); | |||
// assignee2 is the changeAuthor of some notification he's the assignee of | |||
Set<IssueChangeNotification> assignee2ChangeAuthor = randomSetOfNotifications(projectKey, assignee2, assignee2); | |||
Set<IssueChangeNotification> assignee2NotChangeAuthor = randomSetOfNotifications(projectKey, assignee2, randomAlphabetic(10)); | |||
Set<IssueChangeNotification> assignee2NoChangeAuthor = randomSetOfNotifications(projectKey, assignee2, NO_CHANGE_AUTHOR); | |||
// assignee3 is never the changeAuthor of the notification he's the assignee of | |||
Set<IssueChangeNotification> assignee3NotChangeAuthor = randomSetOfNotifications(projectKey, assignee3, randomAlphabetic(11)); | |||
Set<IssueChangeNotification> assignee3NoChangeAuthor = randomSetOfNotifications(projectKey, assignee3, NO_CHANGE_AUTHOR); | |||
@UseDataProvider("userOrAnalysisChange") | |||
public void deliver_ignores_issues_which_assignee_is_the_changeAuthor(Change userOrAnalysisChange) { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
// assignees which are not changeAuthor have subscribed | |||
Set<String> assigneesChangeAuthor = ImmutableSet.of(assignee2, assignee3); | |||
when(notificationManager.findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, assigneesChangeAuthor, ALL_MUST_HAVE_ROLE_USER)) | |||
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee2), emailRecipientOf(assignee3))); | |||
Set<EmailDeliveryRequest> expectedRequests = Stream.of( | |||
assignee2NotChangeAuthor.stream(), assignee2NoChangeAuthor.stream(), | |||
assignee3NotChangeAuthor.stream(), assignee3NoChangeAuthor.stream()) | |||
.flatMap(t -> t) | |||
.map(t -> new EmailDeliveryRequest(emailOf(t.getAssignee()), t)) | |||
Project project1 = newProject(); | |||
Project project2 = newProject(); | |||
User assignee1 = newUser("assignee_1"); | |||
User assignee2 = newUser("assignee_2"); | |||
Set<ChangedIssue> assignee1Issues = IntStream.range(0, 10) | |||
.mapToObj(i -> newChangedIssue("1_issue_key_" + i, assignee1, project1)) | |||
.collect(toSet()); | |||
Set<ChangedIssue> assignee2Issues = IntStream.range(0, 10) | |||
.mapToObj(i -> newChangedIssue("2_issue_key_" + i, assignee2, project2)) | |||
.collect(toSet()); | |||
int deliveredCount = new Random().nextInt(expectedRequests.size()); | |||
when(emailNotificationChannel.deliverAll(expectedRequests)).thenReturn(deliveredCount); | |||
Set<IssueChangeNotification> notifications = Stream.of( | |||
assignee1ChangeAuthor.stream(), | |||
assignee2ChangeAuthor.stream(), assignee2NotChangeAuthor.stream(), assignee2NoChangeAuthor.stream(), | |||
assignee3NotChangeAuthor.stream(), assignee3NoChangeAuthor.stream()).flatMap(t -> t) | |||
UserChange assignee2Change1 = new UserChange(new Random().nextLong(), assignee2); | |||
Set<IssuesChangesNotification> notifications = Stream.of( | |||
// notification from assignee1 with issues from assignee1 only | |||
new IssuesChangesNotificationBuilder( | |||
assignee1Issues.stream().limit(4).collect(toSet()), | |||
new UserChange(new Random().nextLong(), assignee1)), | |||
// notification from assignee2 with issues from assignee1 and assignee2 | |||
new IssuesChangesNotificationBuilder( | |||
Stream.concat( | |||
assignee1Issues.stream().skip(4).limit(2), | |||
assignee2Issues.stream().limit(4)) | |||
.collect(toSet()), | |||
assignee2Change1), | |||
// notification from assignee2 with issues from assignee2 only | |||
new IssuesChangesNotificationBuilder( | |||
assignee2Issues.stream().skip(4).limit(3).collect(toSet()), | |||
new UserChange(new Random().nextLong(), assignee2)), | |||
// notification from other change with issues from assignee1 and assignee2) | |||
new IssuesChangesNotificationBuilder( | |||
Stream.concat( | |||
assignee1Issues.stream().skip(6), | |||
assignee2Issues.stream().skip(7)) | |||
.collect(toSet()), | |||
userOrAnalysisChange)) | |||
.map(t -> serializer.serialize(t)) | |||
.collect(toSet()); | |||
when(notificationManager.findSubscribedEmailRecipients( | |||
CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project1.getKey(), ImmutableSet.of(assignee1.getLogin()), ALL_MUST_HAVE_ROLE_USER)) | |||
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee1.getLogin()))); | |||
when(notificationManager.findSubscribedEmailRecipients( | |||
CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, project2.getKey(), ImmutableSet.of(assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER)) | |||
.thenReturn(ImmutableSet.of(emailRecipientOf(assignee2.getLogin()))); | |||
int deliveredCount = new Random().nextInt(100); | |||
when(emailNotificationChannel.deliverAll(anySet())).thenReturn(deliveredCount); | |||
int deliver = underTest.deliver(notifications); | |||
assertThat(deliver).isEqualTo(deliveredCount); | |||
verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, projectKey, assigneesChangeAuthor, ALL_MUST_HAVE_ROLE_USER); | |||
verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, | |||
project1.getKey(), ImmutableSet.of(assignee1.getLogin()), ALL_MUST_HAVE_ROLE_USER); | |||
verify(notificationManager).findSubscribedEmailRecipients(CHANGE_ON_MY_ISSUES_DISPATCHER_KEY, | |||
project2.getKey(), ImmutableSet.of(assignee2.getLogin()), ALL_MUST_HAVE_ROLE_USER); | |||
verifyNoMoreInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
verify(emailNotificationChannel).deliverAll(expectedRequests); | |||
verify(emailNotificationChannel).deliverAll(emailDeliveryRequestSetCaptor.capture()); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
Set<EmailDeliveryRequest> emailDeliveryRequests = emailDeliveryRequestSetCaptor.getValue(); | |||
assertThat(emailDeliveryRequests).hasSize(3); | |||
ListMultimap<String, EmailDeliveryRequest> emailDeliveryRequestByEmail = emailDeliveryRequests.stream() | |||
.collect(index(EmailDeliveryRequest::getRecipientEmail)); | |||
List<EmailDeliveryRequest> assignee1Requests = emailDeliveryRequestByEmail.get(emailOf(assignee1.getLogin())); | |||
assertThat(assignee1Requests) | |||
.hasSize(2) | |||
.extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification()) | |||
.extracting(ChangesOnMyIssuesNotification::getChange) | |||
.containsOnly(userOrAnalysisChange, assignee2Change1); | |||
assertThat(assignee1Requests) | |||
.extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification()) | |||
.extracting(ChangesOnMyIssuesNotification::getChangedIssues) | |||
.containsOnly( | |||
assignee1Issues.stream().skip(4).limit(2).collect(unorderedIndex(t -> project1, t -> t)), | |||
assignee1Issues.stream().skip(6).collect(unorderedIndex(t -> project1, t -> t))); | |||
List<EmailDeliveryRequest> assignee2Requests = emailDeliveryRequestByEmail.get(emailOf(assignee2.getLogin())); | |||
assertThat(assignee2Requests) | |||
.hasSize(1) | |||
.extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification()) | |||
.extracting(ChangesOnMyIssuesNotification::getChange) | |||
.containsOnly(userOrAnalysisChange); | |||
assertThat(assignee2Requests) | |||
.extracting(t -> (ChangesOnMyIssuesNotification) t.getNotification()) | |||
.extracting(ChangesOnMyIssuesNotification::getChangedIssues) | |||
.containsOnly(assignee2Issues.stream().skip(7).collect(unorderedIndex(t -> project2, t -> t))); | |||
} | |||
@DataProvider | |||
public static Object[][] noOrDifferentChangeAuthor() { | |||
public static Object[][] userOrAnalysisChange() { | |||
User changeAuthor = new User(randomAlphabetic(12), randomAlphabetic(10), randomAlphabetic(11)); | |||
return new Object[][] { | |||
{NO_CHANGE_AUTHOR}, | |||
{randomAlphabetic(15)} | |||
{new AnalysisChange(new Random().nextLong())}, | |||
{new UserChange(new Random().nextLong(), changeAuthor)}, | |||
}; | |||
} | |||
private static Set<IssueChangeNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) { | |||
private static Project newProject() { | |||
String base = randomAlphabetic(6); | |||
return newProject(base); | |||
} | |||
private static Project newProject(String base) { | |||
return new Project.Builder("prj_uuid_" + base) | |||
.setKey("prj_key_" + base) | |||
.setProjectName("prj_name_" + base) | |||
.build(); | |||
} | |||
private static User newUser(String name) { | |||
return new User(name + "_uuid", name + "login", name); | |||
} | |||
private static ChangedIssue newChangedIssue(String key, User assignee1, Project project) { | |||
return new ChangedIssue.Builder(key) | |||
.setNewStatus("foo") | |||
.setAssignee(assignee1) | |||
.setRule(newRule()) | |||
.setProject(project) | |||
.build(); | |||
} | |||
private static Rule newRule() { | |||
return new Rule(RuleKey.of(randomAlphabetic(3), randomAlphabetic(4)), randomAlphabetic(5)); | |||
} | |||
private static Set<IssuesChangesNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) { | |||
return IntStream.range(0, 1 + new Random().nextInt(5)) | |||
.mapToObj(i -> newNotification(projectKey, assignee, changeAuthor)) | |||
.collect(Collectors.toSet()); | |||
} | |||
private static IssueChangeNotification newNotification(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) { | |||
IssueChangeNotification notification = mock(IssueChangeNotification.class); | |||
when(notification.getProjectKey()).thenReturn(projectKey); | |||
when(notification.getAssignee()).thenReturn(assignee); | |||
when(notification.getChangeAuthor()).thenReturn(changeAuthor); | |||
private static IssuesChangesNotification newNotification(@Nullable String projectKey, @Nullable String assignee, @Nullable String changeAuthor) { | |||
IssuesChangesNotification notification = mock(IssuesChangesNotification.class); | |||
return notification; | |||
} | |||
@@ -0,0 +1,721 @@ | |||
/* | |||
* 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 com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.Locale; | |||
import java.util.Random; | |||
import java.util.Set; | |||
import java.util.function.Function; | |||
import java.util.stream.IntStream; | |||
import java.util.stream.Stream; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.i18n.I18n; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import org.sonar.test.html.HtmlFragmentAssert; | |||
import org.sonar.test.html.HtmlListAssert; | |||
import org.sonar.test.html.HtmlParagraphAssert; | |||
import static java.util.stream.Collectors.joining; | |||
import static java.util.stream.Collectors.toList; | |||
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.when; | |||
import static org.sonar.api.issue.Issue.STATUS_CLOSED; | |||
import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; | |||
import static org.sonar.api.issue.Issue.STATUS_OPEN; | |||
import static org.sonar.api.issue.Issue.STATUS_REOPENED; | |||
import static org.sonar.api.issue.Issue.STATUS_RESOLVED; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newBranch; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newChangedIssue; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newRule; | |||
@RunWith(DataProviderRunner.class) | |||
public class ChangesOnMyIssuesEmailTemplateTest { | |||
private static final String[] ISSUE_STATUSES = {STATUS_OPEN, STATUS_RESOLVED, STATUS_CONFIRMED, STATUS_REOPENED, STATUS_CLOSED}; | |||
@org.junit.Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private I18n i18n = mock(I18n.class); | |||
private EmailSettings emailSettings = mock(EmailSettings.class); | |||
private ChangesOnMyIssuesEmailTemplate underTest = new ChangesOnMyIssuesEmailTemplate(i18n, emailSettings); | |||
@Test | |||
public void format_returns_null_on_Notification() { | |||
EmailMessage emailMessage = underTest.format(mock(Notification.class)); | |||
assertThat(emailMessage).isNull(); | |||
} | |||
@Test | |||
public void formats_fails_with_ISE_if_change_from_Analysis_and_no_issue() { | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
expectedException.expect(IllegalStateException.class); | |||
expectedException.expectMessage("changedIssues can't be empty"); | |||
underTest.format(new ChangesOnMyIssuesNotification(analysisChange, Collections.emptySet())); | |||
} | |||
@Test | |||
public void format_sets_message_id_with_project_key_of_first_issue_in_set_when_change_from_Analysis() { | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) | |||
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i))) | |||
.collect(toSet()); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues)); | |||
assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues/" + changedIssues.iterator().next().getProject().getKey()); | |||
} | |||
@Test | |||
public void format_sets_subject_with_project_name_of_first_issue_in_set_when_change_from_Analysis() { | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) | |||
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i))) | |||
.collect(toSet()); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues)); | |||
Project project = changedIssues.iterator().next().getProject(); | |||
assertThat(emailMessage.getSubject()).isEqualTo("Analysis has changed some of your issues in " + project.getProjectName()); | |||
} | |||
@Test | |||
public void format_sets_subject_with_project_name_and_branch_name_of_first_issue_in_set_when_change_from_Analysis() { | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) | |||
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newBranch("prj_" + i, "br_" + i), newRule("rule_" + i))) | |||
.collect(toSet()); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues)); | |||
Project project = changedIssues.iterator().next().getProject(); | |||
assertThat(emailMessage.getSubject()).isEqualTo("Analysis has changed some of your issues in " + project.getProjectName() + ", " + project.getBranchName().get()); | |||
} | |||
@Test | |||
public void format_set_html_message_with_header_dealing_with_plural_when_change_from_Analysis() { | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) | |||
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i))) | |||
.collect(toSet()); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues.stream().limit(1).collect(toSet()))); | |||
EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues)); | |||
HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage()) | |||
.hasParagraph("Hi,") | |||
.hasParagraph("An analysis has updated an issue assigned to you:"); | |||
HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage()) | |||
.hasParagraph("Hi,") | |||
.hasParagraph("An analysis has updated issues assigned to you:"); | |||
} | |||
@Test | |||
public void format_sets_static_message_id_when_change_from_User() { | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) | |||
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i))) | |||
.collect(toSet()); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues)); | |||
assertThat(emailMessage.getMessageId()).isEqualTo("changes-on-my-issues"); | |||
} | |||
@Test | |||
public void format_sets_static_subject_when_change_from_User() { | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) | |||
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i))) | |||
.collect(toSet()); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues)); | |||
assertThat(emailMessage.getSubject()).isEqualTo("A manual update has changed some of your issues"); | |||
} | |||
@Test | |||
public void format_set_html_message_with_header_dealing_with_plural_when_change_from_User() { | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) | |||
.mapToObj(i -> newChangedIssue(i + "", randomValidStatus(), newProject("prj_" + i), newRule("rule_" + i))) | |||
.collect(toSet()); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification( | |||
userChange, changedIssues.stream().limit(1).collect(toSet()))); | |||
EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, changedIssues)); | |||
HtmlFragmentAssert.assertThat(singleIssueMessage.getMessage()) | |||
.hasParagraph("Hi,") | |||
.withoutLink() | |||
.hasParagraph("A manual change has updated an issue assigned to you:") | |||
.withoutLink(); | |||
HtmlFragmentAssert.assertThat(multiIssueMessage.getMessage()) | |||
.hasParagraph("Hi,") | |||
.withoutLink() | |||
.hasParagraph("A manual change has updated issues assigned to you:") | |||
.withoutLink(); | |||
} | |||
@Test | |||
@UseDataProvider("issueStatuses") | |||
public void format_set_html_message_with_footer_when_change_from_user(String issueStatus) { | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
format_set_html_message_with_footer(userChange, issueStatus, c -> c | |||
// skip content | |||
.hasParagraph() // open/closed issue | |||
.hasList() // rule list | |||
); | |||
} | |||
@Test | |||
@UseDataProvider("issueStatuses") | |||
public void format_set_html_message_with_footer_when_change_from_analysis(String issueStatus) { | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
format_set_html_message_with_footer(analysisChange, issueStatus, c -> c | |||
// skip content | |||
.hasParagraph() // status | |||
.hasList() // rule list | |||
); | |||
} | |||
@DataProvider | |||
public static Object[][] issueStatuses() { | |||
return Arrays.stream(ISSUE_STATUSES) | |||
.map(t -> new Object[] {t}) | |||
.toArray(Object[][]::new); | |||
} | |||
private void format_set_html_message_with_footer(Change change, String issueStatus, Function<HtmlParagraphAssert, HtmlListAssert> skipContent) { | |||
String wordingNotification = randomAlphabetic(20); | |||
String host = randomAlphabetic(15); | |||
String instance = randomAlphabetic(17); | |||
when(i18n.message(Locale.ENGLISH, "notification.dispatcher.ChangesOnMyIssue", "notification.dispatcher.ChangesOnMyIssue")) | |||
.thenReturn(wordingNotification); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
when(emailSettings.getInstanceName()).thenReturn(instance); | |||
Project project = newProject("foo"); | |||
Rule rule = newRule("bar"); | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(4)) | |||
.mapToObj(i -> newChangedIssue(i + "", issueStatus, project, rule)) | |||
.collect(toSet()); | |||
EmailMessage singleIssueMessage = underTest.format(new ChangesOnMyIssuesNotification( | |||
change, changedIssues.stream().limit(1).collect(toSet()))); | |||
EmailMessage multiIssueMessage = underTest.format(new ChangesOnMyIssuesNotification(change, changedIssues)); | |||
Stream.of(singleIssueMessage, multiIssueMessage) | |||
.forEach(issueMessage -> { | |||
HtmlParagraphAssert htmlAssert = HtmlFragmentAssert.assertThat(issueMessage.getMessage()) | |||
.hasParagraph().hasParagraph(); // skip header | |||
// skip content | |||
HtmlListAssert htmlListAssert = skipContent.apply(htmlAssert); | |||
String footerText = "You received this email because you are subscribed to \"" + wordingNotification + "\" notifications from " + instance + "." | |||
+ " Click here to edit your email preferences."; | |||
htmlListAssert.hasEmptyParagraph() | |||
.hasParagraph(footerText) | |||
.withSmallOn(footerText) | |||
.withLink("here", host + "/account/notifications") | |||
.noMoreBlock(); | |||
}); | |||
} | |||
@Test | |||
public void format_set_html_message_with_issues_grouped_by_status_closed_or_any_other_when_change_from_analysis() { | |||
Project project = newProject("foo"); | |||
Rule rule = newRule("bar"); | |||
Set<ChangedIssue> changedIssues = Arrays.stream(ISSUE_STATUSES) | |||
.map(status -> newChangedIssue(status + "", status, project, rule)) | |||
.collect(toSet()); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, changedIssues)); | |||
HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph("Closed issue:") | |||
.withoutLink() | |||
.hasList("Rule " + rule.getName() + " - See the single issue") | |||
.withLinkOn("See the single issue") | |||
.hasParagraph("Open issues:") | |||
.withoutLink() | |||
.hasList("Rule " + rule.getName() + " - See all " + (ISSUE_STATUSES.length - 1) + " issues") | |||
.withLinkOn("See all " + (ISSUE_STATUSES.length - 1) + " issues"); | |||
verifyEnd(htmlListAssert); | |||
} | |||
@Test | |||
public void format_set_html_message_with_status_title_handles_plural_when_change_from_analysis() { | |||
Project project = newProject("foo"); | |||
Rule rule = newRule("bar"); | |||
Set<ChangedIssue> closedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(status -> newChangedIssue(status + "", STATUS_CLOSED, project, rule)) | |||
.collect(toSet()); | |||
Set<ChangedIssue> openIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(status -> newChangedIssue(status + "", STATUS_OPEN, project, rule)) | |||
.collect(toSet()); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
EmailMessage closedIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, closedIssues)); | |||
EmailMessage openIssuesMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, openIssues)); | |||
HtmlListAssert htmlListAssert = HtmlFragmentAssert.assertThat(closedIssuesMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph("Closed issues:") | |||
.hasList(); | |||
verifyEnd(htmlListAssert); | |||
htmlListAssert = HtmlFragmentAssert.assertThat(openIssuesMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph("Open issues:") | |||
.hasList(); | |||
verifyEnd(htmlListAssert); | |||
} | |||
@Test | |||
public void formats_returns_html_message_for_single_issue_on_master_when_analysis_change() { | |||
Project project = newProject("1"); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue))); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph()// skip title based on status | |||
.hasList("Rule " + ruleName + " - See the single issue") | |||
.withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey()) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_for_single_issue_on_master_when_user_change() { | |||
Project project = newProject("1"); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
ChangedIssue changedIssue = newChangedIssue("key", randomValidStatus(), project, ruleName); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue))); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName()) | |||
.hasList("Rule " + ruleName + " - See the single issue") | |||
.withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey()) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_for_single_issue_on_branch_when_analysis_change() { | |||
String branchName = randomAlphabetic(6); | |||
Project project = newBranch("1", branchName); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
String key = "key"; | |||
ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue))); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph()// skip title based on status | |||
.hasList("Rule " + ruleName + " - See the single issue") | |||
.withLink("See the single issue", | |||
host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey()) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_for_single_issue_on_branch_when_user_change() { | |||
String branchName = randomAlphabetic(6); | |||
Project project = newBranch("1", branchName); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
String key = "key"; | |||
ChangedIssue changedIssue = newChangedIssue(key, randomValidStatus(), project, ruleName); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.of(changedIssue))); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName() + ", " + branchName) | |||
.hasList("Rule " + ruleName + " - See the single issue") | |||
.withLink("See the single issue", | |||
host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey()) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master_when_analysis_change() { | |||
Project project = newProject("1"); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
Rule rule = newRule(ruleName); | |||
String issueStatus = randomValidStatus(); | |||
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(i -> newChangedIssue("issue_" + i, issueStatus, project, rule)) | |||
.collect(toList()); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues))); | |||
String expectedHref = host + "/project/issues?id=" + project.getKey() | |||
+ "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C")); | |||
String expectedLinkText = "See all " + changedIssues.size() + " issues"; | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph() // skip title based on status | |||
.hasList("Rule " + ruleName + " - " + expectedLinkText) | |||
.withLink(expectedLinkText, expectedHref) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master_when_user_change() { | |||
Project project = newProject("1"); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
Rule rule = newRule(ruleName); | |||
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule)) | |||
.collect(toList()); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues))); | |||
String expectedHref = host + "/project/issues?id=" + project.getKey() | |||
+ "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C")); | |||
String expectedLinkText = "See all " + changedIssues.size() + " issues"; | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName()) | |||
.hasList("Rule " + ruleName + " - " + expectedLinkText) | |||
.withLink(expectedLinkText, expectedHref) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch_when_analysis_change() { | |||
String branchName = randomAlphabetic(19); | |||
Project project = newBranch("1", branchName); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
Rule rule = newRule(ruleName); | |||
String status = randomValidStatus(); | |||
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(i -> newChangedIssue("issue_" + i, status, project, rule)) | |||
.collect(toList()); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues))); | |||
String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName | |||
+ "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C")); | |||
String expectedLinkText = "See all " + changedIssues.size() + " issues"; | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph()// skip title based on status | |||
.hasList("Rule " + ruleName + " - " + expectedLinkText) | |||
.withLink(expectedLinkText, expectedHref) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch_when_user_change() { | |||
String branchName = randomAlphabetic(19); | |||
Project project = newBranch("1", branchName); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
Rule rule = newRule(ruleName); | |||
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(i -> newChangedIssue("issue_" + i, randomValidStatus(), project, rule)) | |||
.collect(toList()); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues))); | |||
String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName | |||
+ "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C")); | |||
String expectedLinkText = "See all " + changedIssues.size() + " issues"; | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName() + ", " + branchName) | |||
.hasList("Rule " + ruleName + " - " + expectedLinkText) | |||
.withLink(expectedLinkText, expectedHref) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_with_projects_ordered_by_name_when_user_change() { | |||
Project project1 = newProject("1"); | |||
Project project1Branch1 = newBranch("1", "a"); | |||
Project project1Branch2 = newBranch("1", "b"); | |||
Project project2 = newProject("B"); | |||
Project project2Branch1 = newBranch("B", "a"); | |||
Project project3 = newProject("C"); | |||
String host = randomAlphabetic(15); | |||
List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3) | |||
.map(project -> newChangedIssue("issue_" + project.getUuid(), randomValidStatus(), project, newRule(randomAlphabetic(2)))) | |||
.collect(toList()); | |||
Collections.shuffle(changedIssues); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues))); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project1.getProjectName()) | |||
.hasList() | |||
.hasParagraph(project1Branch1.getProjectName() + ", " + project1Branch1.getBranchName().get()) | |||
.hasList() | |||
.hasParagraph(project1Branch2.getProjectName() + ", " + project1Branch2.getBranchName().get()) | |||
.hasList() | |||
.hasParagraph(project2.getProjectName()) | |||
.hasList() | |||
.hasParagraph(project2Branch1.getProjectName() + ", " + project2Branch1.getBranchName().get()) | |||
.hasList() | |||
.hasParagraph(project3.getProjectName()) | |||
.hasList() | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change() { | |||
Project project = newProject("1"); | |||
Rule rule1 = newRule("1"); | |||
Rule rule2 = newRule("a"); | |||
Rule rule3 = newRule("b"); | |||
Rule rule4 = newRule("X"); | |||
String host = randomAlphabetic(15); | |||
String issueStatus = randomValidStatus(); | |||
List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4) | |||
.map(rule -> newChangedIssue("issue_" + rule.getName(), issueStatus, project, rule)) | |||
.collect(toList()); | |||
Collections.shuffle(changedIssues); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues))); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph()// skip title based on status | |||
.hasList( | |||
"Rule " + rule1.getName() + " - See the single issue", | |||
"Rule " + rule2.getName() + " - See the single issue", | |||
"Rule " + rule3.getName() + " - See the single issue", | |||
"Rule " + rule4.getName() + " - See the single issue") | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_with_rules_ordered_by_name_when_analysis_change_when_user_analysis() { | |||
Project project = newProject("1"); | |||
Rule rule1 = newRule("1"); | |||
Rule rule2 = newRule("a"); | |||
Rule rule3 = newRule("b"); | |||
Rule rule4 = newRule("X"); | |||
String host = randomAlphabetic(15); | |||
List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4) | |||
.map(rule -> newChangedIssue("issue_" + rule.getName(), randomValidStatus(), project, rule)) | |||
.collect(toList()); | |||
Collections.shuffle(changedIssues); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues))); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName()) | |||
.hasList( | |||
"Rule " + rule1.getName() + " - See the single issue", | |||
"Rule " + rule2.getName() + " - See the single issue", | |||
"Rule " + rule3.getName() + " - See the single issue", | |||
"Rule " + rule4.getName() + " - See the single issue") | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_analysis_change() { | |||
Project project1 = newProject("1"); | |||
Rule rule1 = newRule("1"); | |||
Rule rule2 = newRule("a"); | |||
String host = randomAlphabetic(15); | |||
String issueStatusClosed = STATUS_CLOSED; | |||
String otherIssueStatus = STATUS_RESOLVED; | |||
List<ChangedIssue> changedIssues = Stream.of( | |||
IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, issueStatusClosed, project1, rule1)), | |||
IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, issueStatusClosed, project1, rule2)), | |||
IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, otherIssueStatus, project1, rule2)), | |||
IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, otherIssueStatus, project1, rule1))) | |||
.flatMap(t -> t) | |||
.collect(toList()); | |||
Collections.shuffle(changedIssues); | |||
AnalysisChange analysisChange = IssuesChangesNotificationBuilderTesting.newAnalysisChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.copyOf(changedIssues))); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph("Closed issues:") // skip title based on status | |||
.hasList( | |||
"Rule " + rule1.getName() + " - See all 39 issues", | |||
"Rule " + rule2.getName() + " - See all 40 issues") | |||
.withLink("See all 39 issues", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C"))) | |||
.withLink("See all 40 issues", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C"))) | |||
.hasParagraph("Open issues:") | |||
.hasList( | |||
"Rule " + rule2.getName() + " - See issues 1-40 41-80 81", | |||
"Rule " + rule1.getName() + " - See all 6 issues") | |||
.withLink("1-40", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C"))) | |||
.withLink("41-80", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C"))) | |||
.withLink("81", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9") | |||
.withLink("See all 6 issues", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C"))) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues_when_user_change() { | |||
Project project1 = newProject("1"); | |||
Project project2 = newProject("V"); | |||
Project project2Branch = newBranch("V", "AB"); | |||
Rule rule1 = newRule("1"); | |||
Rule rule2 = newRule("a"); | |||
String status = randomValidStatus(); | |||
String host = randomAlphabetic(15); | |||
List<ChangedIssue> changedIssues = Stream.of( | |||
IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, status, project1, rule1)), | |||
IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, status, project1, rule2)), | |||
IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, status, project2, rule2)), | |||
IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, status, project2Branch, rule1))) | |||
.flatMap(t -> t) | |||
.collect(toList()); | |||
Collections.shuffle(changedIssues); | |||
UserChange userChange = IssuesChangesNotificationBuilderTesting.newUserChange(); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new ChangesOnMyIssuesNotification(userChange, ImmutableSet.copyOf(changedIssues))); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project1.getProjectName()) | |||
.hasList() | |||
.withItemTexts( | |||
"Rule " + rule1.getName() + " - See all 39 issues", | |||
"Rule " + rule2.getName() + " - See all 40 issues") | |||
.withLink("See all 39 issues", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C"))) | |||
.withLink("See all 40 issues", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C"))) | |||
.hasParagraph(project2.getProjectName()) | |||
.hasList("Rule " + rule2.getName() + " - See issues 1-40 41-80 81") | |||
.withLink("1-40", | |||
host + "/project/issues?id=" + project2.getKey() | |||
+ "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C"))) | |||
.withLink("41-80", | |||
host + "/project/issues?id=" + project2.getKey() | |||
+ "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C"))) | |||
.withLink("81", | |||
host + "/project/issues?id=" + project2.getKey() | |||
+ "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9") | |||
.hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get()) | |||
.hasList("Rule " + rule1.getName() + " - See all 6 issues") | |||
.withLink("See all 6 issues", | |||
host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get() | |||
+ "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C"))) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
private static String randomValidStatus() { | |||
return ISSUE_STATUSES[new Random().nextInt(ISSUE_STATUSES.length)]; | |||
} | |||
private void verifyEnd(HtmlListAssert htmlListAssert) { | |||
htmlListAssert | |||
.hasEmptyParagraph() | |||
.hasParagraph() | |||
.noMoreBlock(); | |||
} | |||
} |
@@ -0,0 +1,73 @@ | |||
/* | |||
* 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.Random; | |||
import org.junit.Test; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
public class ChangesOnMyIssuesNotificationTest { | |||
@Test | |||
public void key_is_ChangesOnMyIssues() { | |||
ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification( | |||
new UserChange(new Random().nextLong(), new User(randomAlphabetic(2), randomAlphabetic(3), randomAlphabetic(4))), | |||
ImmutableSet.of()); | |||
assertThat(underTest.getType()).isEqualTo("ChangesOnMyIssues"); | |||
} | |||
@Test | |||
public void equals_is_based_on_change_and_issues() { | |||
AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong()); | |||
ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"), IssuesChangesNotificationBuilderTesting.newRule("rul")); | |||
ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)); | |||
assertThat(underTest) | |||
.isEqualTo(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue))) | |||
.isNotEqualTo(mock(Notification.class)) | |||
.isNotEqualTo(null) | |||
.isNotEqualTo(new ChangesOnMyIssuesNotification(new AnalysisChange(analysisChange.getDate() + 10), ImmutableSet.of(changedIssue))) | |||
.isNotEqualTo(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of())); | |||
} | |||
@Test | |||
public void hashcode_is_based_on_change_and_issues() { | |||
AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong()); | |||
ChangedIssue changedIssue = IssuesChangesNotificationBuilderTesting.newChangedIssue("doo", IssuesChangesNotificationBuilderTesting.newProject("prj"), IssuesChangesNotificationBuilderTesting.newRule("rul")); | |||
ChangesOnMyIssuesNotification underTest = new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)); | |||
assertThat(underTest.hashCode()) | |||
.isEqualTo(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of(changedIssue)).hashCode()) | |||
.isNotEqualTo(mock(Notification.class).hashCode()) | |||
.isNotEqualTo(null) | |||
.isNotEqualTo(new ChangesOnMyIssuesNotification(new AnalysisChange(analysisChange.getDate() + 10), ImmutableSet.of(changedIssue)).hashCode()) | |||
.isNotEqualTo(new ChangesOnMyIssuesNotification(analysisChange, ImmutableSet.of())).hashCode(); | |||
} | |||
} |
@@ -1,293 +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.ImmutableSet; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
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.junit.runner.RunWith; | |||
import org.mockito.Mockito; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.server.notification.NotificationDispatcherMetadata; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.notification.email.EmailNotificationChannel; | |||
import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest; | |||
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; | |||
@RunWith(DataProviderRunner.class) | |||
public class DoNotFixNotificationHandlerTest { | |||
private static final String DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY = "NewFalsePositiveIssue"; | |||
private NotificationManager notificationManager = mock(NotificationManager.class); | |||
private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class); | |||
private DoNotFixNotificationHandler underTest = new DoNotFixNotificationHandler(notificationManager, emailNotificationChannel); | |||
@Test | |||
public void getMetadata_returns_same_instance_as_static_method() { | |||
assertThat(underTest.getMetadata().get()).isSameAs(DoNotFixNotificationHandler.newMetadata()); | |||
} | |||
@Test | |||
public void verify_changeOnMyIssues_notification_dispatcher_key() { | |||
NotificationDispatcherMetadata metadata = DoNotFixNotificationHandler.newMetadata(); | |||
assertThat(metadata.getDispatcherKey()).isEqualTo(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY); | |||
} | |||
@Test | |||
public void changeOnMyIssues_notification_is_disabled_at_global_level() { | |||
NotificationDispatcherMetadata metadata = DoNotFixNotificationHandler.newMetadata(); | |||
assertThat(metadata.getProperty(GLOBAL_NOTIFICATION)).isEqualTo("false"); | |||
} | |||
@Test | |||
public void changeOnMyIssues_notification_is_enable_at_project_level() { | |||
NotificationDispatcherMetadata metadata = DoNotFixNotificationHandler.newMetadata(); | |||
assertThat(metadata.getProperty(PER_PROJECT_NOTIFICATION)).isEqualTo("true"); | |||
} | |||
@Test | |||
public void getNotificationClass_is_IssueChangeNotification() { | |||
assertThat(underTest.getNotificationClass()).isEqualTo(IssueChangeNotification.class); | |||
} | |||
@Test | |||
public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(false); | |||
Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> mock(IssueChangeNotification.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<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> newNotification(null, 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_change_author() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> newNotification(randomAlphabetic(5 + i), 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(); | |||
verify(notification).getChangeAuthor(); | |||
verifyNoMoreInteractions(notification); | |||
}); | |||
} | |||
@Test | |||
public void deliver_has_no_effect_if_no_notification_has_new_resolution() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> newNotification(randomAlphabetic(5 + i), randomAlphabetic(4 + 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).getChangeAuthor(); | |||
verify(notification).getNewResolution(); | |||
verifyNoMoreInteractions(notification); | |||
}); | |||
} | |||
@Test | |||
@UseDataProvider("notFPorWontFixResolution") | |||
public void deliver_has_no_effect_if_no_notification_has_FP_or_wont_fix_resolution(String newResolution) { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<IssueChangeNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> newNotification(randomAlphabetic(5 + i), randomAlphabetic(4 + i), newResolution)) | |||
.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).getChangeAuthor(); | |||
verify(notification).getNewResolution(); | |||
verifyNoMoreInteractions(notification); | |||
}); | |||
} | |||
@DataProvider | |||
public static Object[][] notFPorWontFixResolution() { | |||
return new Object[][] { | |||
{""}, | |||
{randomAlphabetic(9)}, | |||
{Issue.RESOLUTION_FIXED}, | |||
{Issue.RESOLUTION_REMOVED} | |||
}; | |||
} | |||
@Test | |||
@UseDataProvider("FPorWontFixResolution") | |||
public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_FPorWontFix_notifications(String newResolution) { | |||
String projectKey1 = randomAlphabetic(10); | |||
String changeAuthor1 = randomAlphabetic(11); | |||
String projectKey2 = randomAlphabetic(12); | |||
String changeAuthor2 = randomAlphabetic(13); | |||
Set<IssueChangeNotification> notifications1 = randomSetOfNotifications(projectKey1, changeAuthor1, newResolution); | |||
Set<IssueChangeNotification> notifications2 = randomSetOfNotifications(projectKey2, changeAuthor2, newResolution); | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
int deliver = underTest.deliver(Stream.concat(notifications1.stream(), notifications2.stream()).collect(toSet())); | |||
assertThat(deliver).isZero(); | |||
verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey1, ALL_MUST_HAVE_ROLE_USER); | |||
verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey2, ALL_MUST_HAVE_ROLE_USER); | |||
verifyNoMoreInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
} | |||
@Test | |||
@UseDataProvider("FPorWontFixResolution") | |||
public void deliver_does_not_send_email_request_for_notifications_a_subscriber_is_the_changeAuthor_of(String newResolution) { | |||
String projectKey = randomAlphabetic(5); | |||
String subscriber1 = randomAlphabetic(6); | |||
String subscriber2 = randomAlphabetic(7); | |||
String subscriber3 = randomAlphabetic(8); | |||
String otherChangeAuthor = randomAlphabetic(9); | |||
// subscriber1 is the changeAuthor of some notifications | |||
Set<IssueChangeNotification> subscriber1Notifications = randomSetOfNotifications(projectKey, subscriber1, newResolution); | |||
// subscriber2 is the changeAuthor of some notifications | |||
Set<IssueChangeNotification> subscriber2Notifications = randomSetOfNotifications(projectKey, subscriber2, newResolution); | |||
// subscriber3 has no notification | |||
Set<IssueChangeNotification> otherChangeAuthorNotifications = randomSetOfNotifications(projectKey, otherChangeAuthor, newResolution); | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<String> subscribers = ImmutableSet.of(subscriber1, subscriber2, subscriber3); | |||
when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER)) | |||
.thenReturn(subscribers.stream().map(DoNotFixNotificationHandlerTest::emailRecipientOf).collect(toSet())); | |||
Set<EmailDeliveryRequest> expectedRequests = Stream.of( | |||
subscriber1Notifications.stream().flatMap(notif -> Stream.of(subscriber2, subscriber3).map(login -> new EmailDeliveryRequest(emailOf(login), notif))), | |||
subscriber2Notifications.stream().flatMap(notif -> Stream.of(subscriber1, subscriber3).map(login -> new EmailDeliveryRequest(emailOf(login), notif))), | |||
otherChangeAuthorNotifications.stream().flatMap(notif -> Stream.of(subscriber1, subscriber2, subscriber3).map(login -> new EmailDeliveryRequest(emailOf(login), notif)))) | |||
.flatMap(t -> t) | |||
.collect(toSet()); | |||
int deliveredCount = new Random().nextInt(expectedRequests.size()); | |||
when(emailNotificationChannel.deliverAll(expectedRequests)).thenReturn(deliveredCount); | |||
Set<IssueChangeNotification> notifications = Stream.of( | |||
subscriber1Notifications.stream(), | |||
subscriber2Notifications.stream(), | |||
otherChangeAuthorNotifications.stream()) | |||
.flatMap(t -> t) | |||
.collect(toSet()); | |||
int deliver = underTest.deliver(notifications); | |||
assertThat(deliver).isEqualTo(deliveredCount); | |||
verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey, ALL_MUST_HAVE_ROLE_USER); | |||
verifyNoMoreInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
verify(emailNotificationChannel).deliverAll(expectedRequests); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
} | |||
@DataProvider | |||
public static Object[][] FPorWontFixResolution() { | |||
return new Object[][] { | |||
{Issue.RESOLUTION_FALSE_POSITIVE}, | |||
{Issue.RESOLUTION_WONT_FIX} | |||
}; | |||
} | |||
private static Set<IssueChangeNotification> randomSetOfNotifications(@Nullable String projectKey, @Nullable String changeAuthor, @Nullable String newResolution) { | |||
return IntStream.range(0, 1 + new Random().nextInt(5)) | |||
.mapToObj(i -> newNotification(projectKey, changeAuthor, newResolution)) | |||
.collect(Collectors.toSet()); | |||
} | |||
private static IssueChangeNotification newNotification(@Nullable String projectKey, @Nullable String changeAuthor, @Nullable String newResolution) { | |||
IssueChangeNotification notification = mock(IssueChangeNotification.class); | |||
when(notification.getProjectKey()).thenReturn(projectKey); | |||
when(notification.getChangeAuthor()).thenReturn(changeAuthor); | |||
when(notification.getNewResolution()).thenReturn(newResolution); | |||
return notification; | |||
} | |||
private static NotificationManager.EmailRecipient emailRecipientOf(String assignee1) { | |||
return new NotificationManager.EmailRecipient(assignee1, emailOf(assignee1)); | |||
} | |||
private static String emailOf(String assignee1) { | |||
return assignee1 + "@baffe"; | |||
} | |||
} |
@@ -0,0 +1,49 @@ | |||
/* | |||
* 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 org.junit.Test; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class EmailMessageTest { | |||
private EmailMessage underTest = new EmailMessage(); | |||
@Test | |||
public void setHtmlMessage_sets_message_and_html_to_true() { | |||
String message = randomAlphabetic(12); | |||
underTest.setHtmlMessage(message); | |||
assertThat(underTest.getMessage()).isEqualTo(message); | |||
assertThat(underTest.isHtml()).isTrue(); | |||
} | |||
@Test | |||
public void setPlainTextMessage_sets_message_and_html_to_false() { | |||
String message = randomAlphabetic(12); | |||
underTest.setPlainTextMessage(message); | |||
assertThat(underTest.getMessage()).isEqualTo(message); | |||
assertThat(underTest.isHtml()).isFalse(); | |||
} | |||
} |
@@ -0,0 +1,498 @@ | |||
/* | |||
* 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 com.google.common.collect.ListMultimap; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.util.Random; | |||
import java.util.Set; | |||
import java.util.function.Consumer; | |||
import java.util.stream.IntStream; | |||
import java.util.stream.Stream; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.mockito.ArgumentCaptor; | |||
import org.mockito.Mockito; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import org.sonar.server.notification.NotificationDispatcherMetadata; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.notification.email.EmailNotificationChannel; | |||
import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest; | |||
import static java.util.Collections.singleton; | |||
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.junit.Assert.fail; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.ArgumentMatchers.anySet; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.reset; | |||
import static org.mockito.Mockito.spy; | |||
import static org.mockito.Mockito.times; | |||
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.api.issue.Issue.RESOLUTION_FALSE_POSITIVE; | |||
import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX; | |||
import static org.sonar.core.util.stream.MoreCollectors.index; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.newProject; | |||
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; | |||
@RunWith(DataProviderRunner.class) | |||
public class FPOrWontFixNotificationHandlerTest { | |||
private static final String DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY = "NewFalsePositiveIssue"; | |||
private NotificationManager notificationManager = mock(NotificationManager.class); | |||
private EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class); | |||
private IssuesChangesNotificationSerializer serializerMock = mock(IssuesChangesNotificationSerializer.class); | |||
private IssuesChangesNotificationSerializer serializer = spy(new IssuesChangesNotificationSerializer()); | |||
private Class<Set<EmailDeliveryRequest>> requestSetType = (Class<Set<EmailDeliveryRequest>>) (Class<?>) Set.class; | |||
private FPOrWontFixNotificationHandler underTest = new FPOrWontFixNotificationHandler(notificationManager, emailNotificationChannel, serializer); | |||
@Test | |||
public void getMetadata_returns_same_instance_as_static_method() { | |||
assertThat(underTest.getMetadata().get()).isSameAs(FPOrWontFixNotificationHandler.newMetadata()); | |||
} | |||
@Test | |||
public void verify_fpOrWontFixIssues_notification_dispatcher_key() { | |||
NotificationDispatcherMetadata metadata = FPOrWontFixNotificationHandler.newMetadata(); | |||
assertThat(metadata.getDispatcherKey()).isEqualTo(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY); | |||
} | |||
@Test | |||
public void fpOrWontFixIssues_notification_is_disabled_at_global_level() { | |||
NotificationDispatcherMetadata metadata = FPOrWontFixNotificationHandler.newMetadata(); | |||
assertThat(metadata.getProperty(GLOBAL_NOTIFICATION)).isEqualTo("false"); | |||
} | |||
@Test | |||
public void fpOrWontFixIssues_notification_is_enable_at_project_level() { | |||
NotificationDispatcherMetadata metadata = FPOrWontFixNotificationHandler.newMetadata(); | |||
assertThat(metadata.getProperty(PER_PROJECT_NOTIFICATION)).isEqualTo("true"); | |||
} | |||
@Test | |||
public void getNotificationClass_is_IssueChangeNotification() { | |||
assertThat(underTest.getNotificationClass()).isEqualTo(IssuesChangesNotification.class); | |||
} | |||
@Test | |||
public void deliver_has_no_effect_if_emailNotificationChannel_is_disabled() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(false); | |||
Set<IssuesChangesNotification> notifications = IntStream.range(0, 1 + new Random().nextInt(10)) | |||
.mapToObj(i -> mock(IssuesChangesNotification.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_parses_every_notification_in_order() { | |||
Set<IssuesChangesNotification> notifications = IntStream.range(0, 5 + new Random().nextInt(10)) | |||
.mapToObj(i -> mock(IssuesChangesNotification.class)) | |||
.collect(toSet()); | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
when(serializerMock.from(any(IssuesChangesNotification.class))).thenReturn(mock(IssuesChangesNotificationBuilder.class)); | |||
FPOrWontFixNotificationHandler underTest = new FPOrWontFixNotificationHandler(notificationManager, emailNotificationChannel, serializerMock); | |||
underTest.deliver(notifications); | |||
notifications.forEach(notification -> verify(serializerMock).from(notification)); | |||
} | |||
@Test | |||
public void deliver_fails_with_IAE_if_serializer_throws_IAE() { | |||
Set<IssuesChangesNotification> notifications = IntStream.range(0, 3 + new Random().nextInt(10)) | |||
.mapToObj(i -> mock(IssuesChangesNotification.class)) | |||
.collect(toSet()); | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
IllegalArgumentException expected = new IllegalArgumentException("faking serializer#from throwing a IllegalArgumentException"); | |||
when(serializerMock.from(any(IssuesChangesNotification.class))) | |||
.thenReturn(mock(IssuesChangesNotificationBuilder.class)) | |||
.thenReturn(mock(IssuesChangesNotificationBuilder.class)) | |||
.thenThrow(expected); | |||
FPOrWontFixNotificationHandler underTest = new FPOrWontFixNotificationHandler(notificationManager, emailNotificationChannel, serializerMock); | |||
try { | |||
underTest.deliver(notifications); | |||
fail("should have throws IAE"); | |||
} catch (IllegalArgumentException e) { | |||
verify(serializerMock, times(3)).from(any(IssuesChangesNotification.class)); | |||
assertThat(e).isSameAs(expected); | |||
} | |||
} | |||
@Test | |||
public void deliver_has_no_effect_if_no_issue_has_new_resolution() { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Change changeMock = mock(Change.class); | |||
Set<IssuesChangesNotification> notifications = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setNewResolution(null)).collect(toSet()), changeMock)) | |||
.map(serializer::serialize) | |||
.collect(toSet()); | |||
reset(serializer); | |||
int deliver = underTest.deliver(notifications); | |||
assertThat(deliver).isZero(); | |||
verify(serializer, times(notifications.size())).from(any(IssuesChangesNotification.class)); | |||
verifyZeroInteractions(changeMock); | |||
verifyNoMoreInteractions(serializer); | |||
verifyZeroInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
} | |||
@Test | |||
@UseDataProvider("notFPorWontFixResolution") | |||
public void deliver_has_no_effect_if_no_issue_has_FP_or_wontfix_resolution(String newResolution) { | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Change changeMock = mock(Change.class); | |||
Set<IssuesChangesNotification> notifications = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setNewResolution(newResolution)).collect(toSet()), changeMock)) | |||
.map(serializer::serialize) | |||
.collect(toSet()); | |||
reset(serializer); | |||
int deliver = underTest.deliver(notifications); | |||
assertThat(deliver).isZero(); | |||
verify(serializer, times(notifications.size())).from(any(IssuesChangesNotification.class)); | |||
verifyZeroInteractions(changeMock); | |||
verifyNoMoreInteractions(serializer); | |||
verifyZeroInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
} | |||
@DataProvider | |||
public static Object[][] notFPorWontFixResolution() { | |||
return new Object[][] { | |||
{""}, | |||
{randomAlphabetic(9)}, | |||
{Issue.RESOLUTION_FIXED}, | |||
{Issue.RESOLUTION_REMOVED} | |||
}; | |||
} | |||
@Test | |||
@UseDataProvider("FPorWontFixResolution") | |||
public void deliver_checks_by_projectKey_if_notifications_have_subscribed_assignee_to_FPorWontFix_notifications(String newResolution) { | |||
Project projectKey1 = newProject(randomAlphabetic(4)); | |||
Project projectKey2 = newProject(randomAlphabetic(5)); | |||
Project projectKey3 = newProject(randomAlphabetic(6)); | |||
Project projectKey4 = newProject(randomAlphabetic(7)); | |||
Change changeMock = mock(Change.class); | |||
// some notifications with some issues on project1 | |||
Stream<IssuesChangesNotificationBuilder> project1Notifications = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder( | |||
randomIssues(t -> t.setProject(projectKey1).setNewResolution(newResolution)).collect(toSet()), | |||
changeMock)); | |||
// some notifications with some issues on project2 | |||
Stream<IssuesChangesNotificationBuilder> project2Notifications = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder( | |||
randomIssues(t -> t.setProject(projectKey2).setNewResolution(newResolution)).collect(toSet()), | |||
changeMock)); | |||
// some notifications with some issues on project3 and project 4 | |||
Stream<IssuesChangesNotificationBuilder> project3And4Notifications = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder( | |||
Stream.concat( | |||
randomIssues(t -> t.setProject(projectKey3).setNewResolution(newResolution)), | |||
randomIssues(t -> t.setProject(projectKey4).setNewResolution(newResolution))) | |||
.collect(toSet()), | |||
changeMock)); | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<IssuesChangesNotification> notifications = Stream.of(project1Notifications, project2Notifications, project3And4Notifications) | |||
.flatMap(t -> t) | |||
.map(serializer::serialize) | |||
.collect(toSet()); | |||
int deliver = underTest.deliver(notifications); | |||
assertThat(deliver).isZero(); | |||
verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey1.getKey(), ALL_MUST_HAVE_ROLE_USER); | |||
verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey2.getKey(), ALL_MUST_HAVE_ROLE_USER); | |||
verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey3.getKey(), ALL_MUST_HAVE_ROLE_USER); | |||
verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, projectKey4.getKey(), ALL_MUST_HAVE_ROLE_USER); | |||
verifyNoMoreInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
verifyZeroInteractions(changeMock); | |||
} | |||
@Test | |||
@UseDataProvider("FPorWontFixResolution") | |||
public void deliver_does_not_send_email_request_for_notifications_a_subscriber_is_the_changeAuthor_of(String newResolution) { | |||
Project project = newProject(randomAlphabetic(5)); | |||
User subscriber1 = newUser("subscriber1"); | |||
User subscriber2 = newUser("subscriber2"); | |||
User subscriber3 = newUser("subscriber3"); | |||
User otherChangeAuthor = newUser("otherChangeAuthor"); | |||
// subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 only | |||
Set<IssuesChangesNotificationBuilder> subscriber1Notifications = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder( | |||
randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)).collect(toSet()), | |||
newUserChange(subscriber1))) | |||
.collect(toSet()); | |||
// subscriber1 is the changeAuthor of some notifications with issues assigned to subscriber1 and subscriber2 | |||
Set<IssuesChangesNotificationBuilder> subscriber1and2Notifications = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder( | |||
Stream.concat( | |||
randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)), | |||
randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber1))) | |||
.collect(toSet()), | |||
newUserChange(subscriber1))) | |||
.collect(toSet()); | |||
// subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 only | |||
Set<IssuesChangesNotificationBuilder> subscriber2Notifications = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder( | |||
randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)).collect(toSet()), | |||
newUserChange(subscriber2))) | |||
.collect(toSet()); | |||
// subscriber2 is the changeAuthor of some notifications with issues assigned to subscriber2 and subscriber 3 | |||
Set<IssuesChangesNotificationBuilder> subscriber2And3Notifications = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder( | |||
Stream.concat( | |||
randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber2)), | |||
randomIssues(t -> t.setProject(project).setNewResolution(newResolution).setAssignee(subscriber3))) | |||
.collect(toSet()), | |||
newUserChange(subscriber2))) | |||
.collect(toSet()); | |||
// subscriber3 is the changeAuthor of no notification | |||
// otherChangeAuthor has some notifications | |||
Set<IssuesChangesNotificationBuilder> otherChangeAuthorNotifications = IntStream.range(0, 1 + new Random().nextInt(2)) | |||
.mapToObj(j -> new IssuesChangesNotificationBuilder(randomIssues(t -> t.setProject(project).setNewResolution(newResolution)).collect(toSet()), | |||
newUserChange(otherChangeAuthor))) | |||
.collect(toSet()); | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
Set<String> subscriberLogins = ImmutableSet.of(subscriber1.getLogin(), subscriber2.getLogin(), subscriber3.getLogin()); | |||
when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER)) | |||
.thenReturn(subscriberLogins.stream().map(FPOrWontFixNotificationHandlerTest::emailRecipientOf).collect(toSet())); | |||
int deliveredCount = new Random().nextInt(200); | |||
when(emailNotificationChannel.deliverAll(anySet())) | |||
.thenReturn(deliveredCount) | |||
.thenThrow(new IllegalStateException("deliver should be called only once")); | |||
Set<IssuesChangesNotification> notifications = Stream.of( | |||
subscriber1Notifications.stream(), | |||
subscriber1and2Notifications.stream(), | |||
subscriber2Notifications.stream(), | |||
subscriber2And3Notifications.stream(), | |||
otherChangeAuthorNotifications.stream()) | |||
.flatMap(t -> t) | |||
.map(serializer::serialize) | |||
.collect(toSet()); | |||
reset(serializer); | |||
int deliver = underTest.deliver(notifications); | |||
assertThat(deliver).isEqualTo(deliveredCount); | |||
verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER); | |||
verifyNoMoreInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
ArgumentCaptor<Set<EmailDeliveryRequest>> captor = ArgumentCaptor.forClass(requestSetType); | |||
verify(emailNotificationChannel).deliverAll(captor.capture()); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
ListMultimap<String, EmailDeliveryRequest> requestsByRecipientEmail = captor.getValue().stream() | |||
.collect(index(EmailDeliveryRequest::getRecipientEmail)); | |||
assertThat(requestsByRecipientEmail.get(emailOf(subscriber1.getLogin()))) | |||
.containsOnly( | |||
Stream.of( | |||
subscriber2Notifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrWontFix(newResolution))), | |||
subscriber2And3Notifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrWontFix(newResolution))), | |||
otherChangeAuthorNotifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber1, toFpOrWontFix(newResolution)))) | |||
.flatMap(t -> t) | |||
.toArray(EmailDeliveryRequest[]::new)); | |||
assertThat(requestsByRecipientEmail.get(emailOf(subscriber2.getLogin()))) | |||
.containsOnly( | |||
Stream.of( | |||
subscriber1Notifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrWontFix(newResolution))), | |||
subscriber1and2Notifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrWontFix(newResolution))), | |||
otherChangeAuthorNotifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber2, toFpOrWontFix(newResolution)))) | |||
.flatMap(t -> t) | |||
.toArray(EmailDeliveryRequest[]::new)); | |||
assertThat(requestsByRecipientEmail.get(emailOf(subscriber3.getLogin()))) | |||
.containsOnly( | |||
Stream.of( | |||
subscriber1Notifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))), | |||
subscriber1and2Notifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))), | |||
subscriber2Notifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))), | |||
subscriber2And3Notifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution))), | |||
otherChangeAuthorNotifications.stream() | |||
.map(notif -> newEmailDeliveryRequest(notif, subscriber3, toFpOrWontFix(newResolution)))) | |||
.flatMap(t -> t) | |||
.toArray(EmailDeliveryRequest[]::new)); | |||
assertThat(requestsByRecipientEmail.get(emailOf(otherChangeAuthor.getLogin()))) | |||
.isEmpty(); | |||
} | |||
@Test | |||
@UseDataProvider("oneOrMoreProjectCounts") | |||
public void deliver_send_a_separated_email_request_for_FPs_and_Wont_Fix_issues(int projectCount) { | |||
Set<Project> projects = IntStream.range(0, projectCount).mapToObj(i -> newProject("prk_key_" + i)).collect(toSet()); | |||
User subscriber1 = newUser("subscriber1"); | |||
User changeAuthor = newUser("changeAuthor"); | |||
Set<ChangedIssue> fpIssues = projects.stream() | |||
.flatMap(project -> randomIssues(t -> t.setProject(project).setNewResolution(RESOLUTION_FALSE_POSITIVE).setAssignee(subscriber1))) | |||
.collect(toSet()); | |||
Set<ChangedIssue> wontFixIssues = projects.stream() | |||
.flatMap(project -> randomIssues(t -> t.setProject(project).setNewResolution(RESOLUTION_WONT_FIX).setAssignee(subscriber1))) | |||
.collect(toSet()); | |||
UserChange userChange = newUserChange(changeAuthor); | |||
IssuesChangesNotificationBuilder fpAndWontFixNotifications = new IssuesChangesNotificationBuilder( | |||
Stream.concat(fpIssues.stream(), wontFixIssues.stream()).collect(toSet()), | |||
userChange); | |||
when(emailNotificationChannel.isActivated()).thenReturn(true); | |||
projects.forEach(project -> when(notificationManager.findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER)) | |||
.thenReturn(singleton(emailRecipientOf(subscriber1.getLogin())))); | |||
int deliveredCount = new Random().nextInt(200); | |||
when(emailNotificationChannel.deliverAll(anySet())) | |||
.thenReturn(deliveredCount) | |||
.thenThrow(new IllegalStateException("deliver should be called only once")); | |||
Set<IssuesChangesNotification> notifications = singleton(serializer.serialize(fpAndWontFixNotifications)); | |||
reset(serializer); | |||
int deliver = underTest.deliver(notifications); | |||
assertThat(deliver).isEqualTo(deliveredCount); | |||
projects | |||
.forEach(project -> verify(notificationManager).findSubscribedEmailRecipients(DO_NOT_FIX_ISSUE_CHANGE_DISPATCHER_KEY, project.getKey(), ALL_MUST_HAVE_ROLE_USER)); | |||
verifyNoMoreInteractions(notificationManager); | |||
verify(emailNotificationChannel).isActivated(); | |||
ArgumentCaptor<Set<EmailDeliveryRequest>> captor = ArgumentCaptor.forClass(requestSetType); | |||
verify(emailNotificationChannel).deliverAll(captor.capture()); | |||
verifyNoMoreInteractions(emailNotificationChannel); | |||
ListMultimap<String, EmailDeliveryRequest> requestsByRecipientEmail = captor.getValue().stream() | |||
.collect(index(EmailDeliveryRequest::getRecipientEmail)); | |||
assertThat(requestsByRecipientEmail.get(emailOf(subscriber1.getLogin()))) | |||
.containsOnly( | |||
new EmailDeliveryRequest(emailOf(subscriber1.getLogin()), new FPOrWontFixNotification( | |||
userChange, wontFixIssues, FpOrWontFix.WONT_FIX)), | |||
new EmailDeliveryRequest(emailOf(subscriber1.getLogin()), new FPOrWontFixNotification( | |||
userChange, fpIssues, FpOrWontFix.FP))); | |||
} | |||
@DataProvider | |||
public static Object[][] oneOrMoreProjectCounts() { | |||
return new Object[][] { | |||
{1}, | |||
{2 + new Random().nextInt(3)}, | |||
}; | |||
} | |||
private static EmailDeliveryRequest newEmailDeliveryRequest(IssuesChangesNotificationBuilder notif, User user, FpOrWontFix resolution) { | |||
return new EmailDeliveryRequest( | |||
emailOf(user.getLogin()), | |||
new FPOrWontFixNotification(notif.getChange(), notif.getIssues(), resolution)); | |||
} | |||
private static FpOrWontFix toFpOrWontFix(String newResolution) { | |||
if (newResolution.equals(Issue.RESOLUTION_WONT_FIX)) { | |||
return FpOrWontFix.WONT_FIX; | |||
} | |||
if (newResolution.equals(RESOLUTION_FALSE_POSITIVE)) { | |||
return FpOrWontFix.FP; | |||
} | |||
throw new IllegalArgumentException("unsupported resolution " + newResolution); | |||
} | |||
private static long counter = 233_343; | |||
private static UserChange newUserChange(User subscriber1) { | |||
return new UserChange(counter += 100, subscriber1); | |||
} | |||
public User newUser(String subscriber1) { | |||
return new User(subscriber1, subscriber1 + "_login", subscriber1 + "_name"); | |||
} | |||
@DataProvider | |||
public static Object[][] FPorWontFixResolution() { | |||
return new Object[][] { | |||
{RESOLUTION_FALSE_POSITIVE}, | |||
{Issue.RESOLUTION_WONT_FIX} | |||
}; | |||
} | |||
private static Stream<ChangedIssue> randomIssues(Consumer<ChangedIssue.Builder> consumer) { | |||
return IntStream.range(0, 1 + new Random().nextInt(5)) | |||
.mapToObj(i -> { | |||
ChangedIssue.Builder builder = new ChangedIssue.Builder("key_" + i) | |||
.setAssignee(new User(randomAlphabetic(3), randomAlphabetic(4), randomAlphabetic(5))) | |||
.setNewStatus(randomAlphabetic(12)) | |||
.setNewResolution(randomAlphabetic(13)) | |||
.setRule(new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), randomAlphabetic(8))) | |||
.setProject(new Project.Builder(randomAlphabetic(9)) | |||
.setKey(randomAlphabetic(10)) | |||
.setProjectName(randomAlphabetic(11)) | |||
.build()); | |||
consumer.accept(builder); | |||
return builder.build(); | |||
}); | |||
} | |||
private static NotificationManager.EmailRecipient emailRecipientOf(String assignee1) { | |||
return new NotificationManager.EmailRecipient(assignee1, emailOf(assignee1)); | |||
} | |||
private static String emailOf(String assignee1) { | |||
return assignee1 + "@baffe"; | |||
} | |||
} |
@@ -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.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 org.junit.Test; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP; | |||
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX; | |||
public class FPOrWontFixNotificationTest { | |||
@Test | |||
public void equals_is_based_on_issues_change_and_resolution() { | |||
Rule rule = new Rule(RuleKey.of("repo", "rule_key"), "rule_name"); | |||
Project project = new Project.Builder("prj_uuid").setKey("prj_key").setProjectName("prj_name").build(); | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(i -> new ChangedIssue.Builder("key_" + i) | |||
.setNewStatus("status") | |||
.setRule(rule) | |||
.setProject(project) | |||
.build()) | |||
.collect(Collectors.toSet()); | |||
AnalysisChange change = new AnalysisChange(12); | |||
User user = new User("uuid", "login", null); | |||
FPOrWontFixNotification underTest = new FPOrWontFixNotification(change, changedIssues, WONT_FIX); | |||
assertThat(underTest) | |||
.isEqualTo(new FPOrWontFixNotification(change, changedIssues, WONT_FIX)) | |||
.isEqualTo(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), WONT_FIX)) | |||
.isNotEqualTo(new Object()) | |||
.isNotEqualTo(null) | |||
.isNotEqualTo(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX)) | |||
.isNotEqualTo(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssues.iterator().next()), WONT_FIX)) | |||
.isNotEqualTo(new FPOrWontFixNotification(new AnalysisChange(14), changedIssues, WONT_FIX)) | |||
.isNotEqualTo(new FPOrWontFixNotification(new IssuesChangesNotificationBuilder.UserChange(12, user), changedIssues, WONT_FIX)) | |||
.isNotEqualTo(new FPOrWontFixNotification(change, changedIssues, FP)); | |||
} | |||
@Test | |||
public void hashcode_is_based_on_issues_change_and_resolution() { | |||
Rule rule = new Rule(RuleKey.of("repo", "rule_key"), "rule_name"); | |||
Project project = new Project.Builder("prj_uuid").setKey("prj_key").setProjectName("prj_name").build(); | |||
Set<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(i -> new ChangedIssue.Builder("key_" + i) | |||
.setNewStatus("status") | |||
.setRule(rule) | |||
.setProject(project) | |||
.build()) | |||
.collect(Collectors.toSet()); | |||
AnalysisChange change = new AnalysisChange(12); | |||
User user = new User("uuid", "login", null); | |||
FPOrWontFixNotification underTest = new FPOrWontFixNotification(change, changedIssues, WONT_FIX); | |||
assertThat(underTest.hashCode()) | |||
.isEqualTo(new FPOrWontFixNotification(change, changedIssues, WONT_FIX).hashCode()) | |||
.isEqualTo(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), WONT_FIX).hashCode()) | |||
.isNotEqualTo(new Object().hashCode()) | |||
.isNotEqualTo(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX).hashCode()) | |||
.isNotEqualTo(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssues.iterator().next()), WONT_FIX).hashCode()) | |||
.isNotEqualTo(new FPOrWontFixNotification(new AnalysisChange(14), changedIssues, WONT_FIX).hashCode()) | |||
.isNotEqualTo(new FPOrWontFixNotification(new IssuesChangesNotificationBuilder.UserChange(12, user), changedIssues, WONT_FIX).hashCode()) | |||
.isNotEqualTo(new FPOrWontFixNotification(change, changedIssues, FP)).hashCode(); | |||
} | |||
} |
@@ -0,0 +1,421 @@ | |||
/* | |||
* 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 com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; | |||
import com.tngtech.java.junit.dataprovider.UseDataProvider; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import java.util.Locale; | |||
import java.util.Random; | |||
import java.util.stream.IntStream; | |||
import java.util.stream.Stream; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.i18n.I18n; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Change; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import org.sonar.test.html.HtmlFragmentAssert; | |||
import static java.util.stream.Collectors.joining; | |||
import static java.util.stream.Collectors.toList; | |||
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.when; | |||
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.FP; | |||
import static org.sonar.server.issue.notification.FPOrWontFixNotification.FpOrWontFix.WONT_FIX; | |||
@RunWith(DataProviderRunner.class) | |||
public class FpOrWontFixEmailTemplateTest { | |||
private I18n i18n = mock(I18n.class); | |||
private EmailSettings emailSettings = mock(EmailSettings.class); | |||
private FpOrWontFixEmailTemplate underTest = new FpOrWontFixEmailTemplate(i18n, emailSettings); | |||
@Test | |||
public void format_returns_null_on_Notification() { | |||
EmailMessage emailMessage = underTest.format(mock(Notification.class)); | |||
assertThat(emailMessage).isNull(); | |||
} | |||
@Test | |||
public void format_sets_message_id_specific_to_fp() { | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(mock(Change.class), Collections.emptySet(), FP)); | |||
assertThat(emailMessage.getMessageId()).isEqualTo("fp-issue-changes"); | |||
} | |||
@Test | |||
public void format_sets_message_id_specific_to_wont_fix() { | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(mock(Change.class), Collections.emptySet(), WONT_FIX)); | |||
assertThat(emailMessage.getMessageId()).isEqualTo("wontfix-issue-changes"); | |||
} | |||
@Test | |||
public void format_sets_subject_specific_to_fp() { | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(mock(Change.class), Collections.emptySet(), FP)); | |||
assertThat(emailMessage.getSubject()).isEqualTo("Issues marked as False Positive"); | |||
} | |||
@Test | |||
public void format_sets_subject_specific_to_wont_fix() { | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(mock(Change.class), Collections.emptySet(), WONT_FIX)); | |||
assertThat(emailMessage.getSubject()).isEqualTo("Issues marked as Won't Fix"); | |||
} | |||
@Test | |||
public void format_sets_from_to_name_of_author_change_when_available() { | |||
UserChange change = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6), randomAlphabetic(7))); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX)); | |||
assertThat(emailMessage.getFrom()).isEqualTo(change.getUser().getName().get()); | |||
} | |||
@Test | |||
public void format_sets_from_to_login_of_author_change_when_name_is_not_available() { | |||
UserChange change = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6), null)); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX)); | |||
assertThat(emailMessage.getFrom()).isEqualTo(change.getUser().getLogin()); | |||
} | |||
@Test | |||
public void format_sets_from_to_null_when_analysisChange() { | |||
AnalysisChange change = new AnalysisChange(new Random().nextLong()); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, Collections.emptySet(), WONT_FIX)); | |||
assertThat(emailMessage.getFrom()).isNull(); | |||
} | |||
@Test | |||
@UseDataProvider("userOrAnalysisChange") | |||
public void formats_returns_html_message_with_only_footer_and_header_when_no_issue_for_FPs(Change change) { | |||
formats_returns_html_message_with_only_footer_and_header_when_no_issue(change, FP, "False Positive"); | |||
} | |||
@Test | |||
@UseDataProvider("userOrAnalysisChange") | |||
public void formats_returns_html_message_with_only_footer_and_header_when_no_issue_for_Wont_fixs(Change change) { | |||
formats_returns_html_message_with_only_footer_and_header_when_no_issue(change, WONT_FIX, "Won't Fix"); | |||
} | |||
public void formats_returns_html_message_with_only_footer_and_header_when_no_issue(Change change, FpOrWontFix fpOrWontFix, String fpOrWontFixLabel) { | |||
String wordingNotification = randomAlphabetic(20); | |||
String host = randomAlphabetic(15); | |||
String instance = randomAlphabetic(17); | |||
when(i18n.message(Locale.ENGLISH, "notification.dispatcher.NewFalsePositiveIssue", "notification.dispatcher.NewFalsePositiveIssue")) | |||
.thenReturn(wordingNotification); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
when(emailSettings.getInstanceName()).thenReturn(instance); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, Collections.emptySet(), fpOrWontFix)); | |||
String footerText = "You received this email because you are subscribed to \"" + wordingNotification + "\" notifications from " + instance + "." | |||
+ " Click here to edit your email preferences."; | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph("Hi,") | |||
.withoutLink() | |||
.hasParagraph("A manual change has resolved an issue as " + fpOrWontFixLabel + ":") | |||
.withoutLink() | |||
.hasEmptyParagraph() | |||
.hasParagraph(footerText) | |||
.withSmallOn(footerText) | |||
.withLink("here", host + "/account/notifications") | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") | |||
public void formats_returns_html_message_for_single_issue_on_master(Change change, FpOrWontFix fpOrWontFix) { | |||
Project project = newProject("1"); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
ChangedIssue changedIssue = newChangedIssue("key", project, ruleName); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix)); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName()) | |||
.hasList("Rule " + ruleName + " - See the single issue") | |||
.withLink("See the single issue", host + "/project/issues?id=" + project.getKey() + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey()) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") | |||
public void formats_returns_html_message_for_single_issue_on_branch(Change change, FpOrWontFix fpOrWontFix) { | |||
String branchName = randomAlphabetic(6); | |||
Project project = newBranch("1", branchName); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
String key = "key"; | |||
ChangedIssue changedIssue = newChangedIssue(key, project, ruleName); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.of(changedIssue), fpOrWontFix)); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName() + ", " + branchName) | |||
.hasList("Rule " + ruleName + " - See the single issue") | |||
.withLink("See the single issue", | |||
host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName + "&issues=" + changedIssue.getKey() + "&open=" + changedIssue.getKey()) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") | |||
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_master(Change change, FpOrWontFix fpOrWontFix) { | |||
Project project = newProject("1"); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
Rule rule = newRule(ruleName); | |||
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(i -> newChangedIssue("issue_" + i, project, rule)) | |||
.collect(toList()); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix)); | |||
String expectedHref = host + "/project/issues?id=" + project.getKey() | |||
+ "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C")); | |||
String expectedLinkText = "See all " + changedIssues.size() + " issues"; | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName()) | |||
.hasList("Rule " + ruleName + " - " + expectedLinkText) | |||
.withLink(expectedLinkText, expectedHref) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") | |||
public void formats_returns_html_message_for_multiple_issues_of_same_rule_on_same_project_on_branch(Change change, FpOrWontFix fpOrWontFix) { | |||
String branchName = randomAlphabetic(19); | |||
Project project = newBranch("1", branchName); | |||
String ruleName = randomAlphabetic(8); | |||
String host = randomAlphabetic(15); | |||
Rule rule = newRule(ruleName); | |||
List<ChangedIssue> changedIssues = IntStream.range(0, 2 + new Random().nextInt(5)) | |||
.mapToObj(i -> newChangedIssue("issue_" + i, project, rule)) | |||
.collect(toList()); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix)); | |||
String expectedHref = host + "/project/issues?id=" + project.getKey() + "&branch=" + branchName | |||
+ "&issues=" + changedIssues.stream().map(ChangedIssue::getKey).collect(joining("%2C")); | |||
String expectedLinkText = "See all " + changedIssues.size() + " issues"; | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName() + ", " + branchName) | |||
.hasList("Rule " + ruleName + " - " + expectedLinkText) | |||
.withLink(expectedLinkText, expectedHref) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") | |||
public void formats_returns_html_message_with_projects_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) { | |||
Project project1 = newProject("1"); | |||
Project project1Branch1 = newBranch("1", "a"); | |||
Project project1Branch2 = newBranch("1", "b"); | |||
Project project2 = newProject("B"); | |||
Project project2Branch1 = newBranch("B", "a"); | |||
Project project3 = newProject("C"); | |||
String host = randomAlphabetic(15); | |||
List<ChangedIssue> changedIssues = Stream.of(project1, project1Branch1, project1Branch2, project2, project2Branch1, project3) | |||
.map(project -> newChangedIssue("issue_" + project.getUuid(), project, newRule(randomAlphabetic(2)))) | |||
.collect(toList()); | |||
Collections.shuffle(changedIssues); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix)); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project1.getProjectName()) | |||
.hasList() | |||
.hasParagraph(project1Branch1.getProjectName() + ", " + project1Branch1.getBranchName().get()) | |||
.hasList() | |||
.hasParagraph(project1Branch2.getProjectName() + ", " + project1Branch2.getBranchName().get()) | |||
.hasList() | |||
.hasParagraph(project2.getProjectName()) | |||
.hasList() | |||
.hasParagraph(project2Branch1.getProjectName() + ", " + project2Branch1.getBranchName().get()) | |||
.hasList() | |||
.hasParagraph(project3.getProjectName()) | |||
.hasList() | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") | |||
public void formats_returns_html_message_with_rules_ordered_by_name(Change change, FpOrWontFix fpOrWontFix) { | |||
Project project = newProject("1"); | |||
Rule rule1 = newRule("1"); | |||
Rule rule2 = newRule("a"); | |||
Rule rule3 = newRule("b"); | |||
Rule rule4 = newRule("X"); | |||
String host = randomAlphabetic(15); | |||
List<ChangedIssue> changedIssues = Stream.of(rule1, rule2, rule3, rule4) | |||
.map(rule -> newChangedIssue("issue_" + rule.getName(), project, rule)) | |||
.collect(toList()); | |||
Collections.shuffle(changedIssues); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix)); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project.getProjectName()) | |||
.hasList( | |||
"Rule " + rule1.getName() + " - See the single issue", | |||
"Rule " + rule2.getName() + " - See the single issue", | |||
"Rule " + rule3.getName() + " - See the single issue", | |||
"Rule " + rule4.getName() + " - See the single issue") | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@Test | |||
@UseDataProvider("fpOrWontFixValuesByUserOrAnalysisChange") | |||
public void formats_returns_html_message_with_multiple_links_by_rule_of_groups_of_up_to_40_issues(Change change, FpOrWontFix fpOrWontFix) { | |||
Project project1 = newProject("1"); | |||
Project project2 = newProject("V"); | |||
Project project2Branch = newBranch("V", "AB"); | |||
Rule rule1 = newRule("1"); | |||
Rule rule2 = newRule("a"); | |||
String host = randomAlphabetic(15); | |||
List<ChangedIssue> changedIssues = Stream.of( | |||
IntStream.range(0, 39).mapToObj(i -> newChangedIssue("39_" + i, project1, rule1)), | |||
IntStream.range(0, 40).mapToObj(i -> newChangedIssue("40_" + i, project1, rule2)), | |||
IntStream.range(0, 81).mapToObj(i -> newChangedIssue("1-40_41-80_1_" + i, project2, rule2)), | |||
IntStream.range(0, 6).mapToObj(i -> newChangedIssue("6_" + i, project2Branch, rule1))) | |||
.flatMap(t -> t) | |||
.collect(toList()); | |||
Collections.shuffle(changedIssues); | |||
when(emailSettings.getServerBaseURL()).thenReturn(host); | |||
EmailMessage emailMessage = underTest.format(new FPOrWontFixNotification(change, ImmutableSet.copyOf(changedIssues), fpOrWontFix)); | |||
HtmlFragmentAssert.assertThat(emailMessage.getMessage()) | |||
.hasParagraph().hasParagraph() // skip header | |||
.hasParagraph(project1.getProjectName()) | |||
.hasList() | |||
.withItemTexts( | |||
"Rule " + rule1.getName() + " - See all 39 issues", | |||
"Rule " + rule2.getName() + " - See all 40 issues") | |||
.withLink("See all 39 issues", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + IntStream.range(0, 39).mapToObj(i -> "39_" + i).sorted().collect(joining("%2C"))) | |||
.withLink("See all 40 issues", | |||
host + "/project/issues?id=" + project1.getKey() | |||
+ "&issues=" + IntStream.range(0, 40).mapToObj(i -> "40_" + i).sorted().collect(joining("%2C"))) | |||
.hasParagraph(project2.getProjectName()) | |||
.hasList("Rule " + rule2.getName() + " - See issues 1-40 41-80 81") | |||
.withLink("1-40", | |||
host + "/project/issues?id=" + project2.getKey() | |||
+ "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().limit(40).collect(joining("%2C"))) | |||
.withLink("41-80", | |||
host + "/project/issues?id=" + project2.getKey() | |||
+ "&issues=" + IntStream.range(0, 81).mapToObj(i -> "1-40_41-80_1_" + i).sorted().skip(40).limit(40).collect(joining("%2C"))) | |||
.withLink("81", | |||
host + "/project/issues?id=" + project2.getKey() | |||
+ "&issues=" + "1-40_41-80_1_9" + "&open=" + "1-40_41-80_1_9") | |||
.hasParagraph(project2Branch.getProjectName() + ", " + project2Branch.getBranchName().get()) | |||
.hasList("Rule " + rule1.getName() + " - See all 6 issues") | |||
.withLink("See all 6 issues", | |||
host + "/project/issues?id=" + project2Branch.getKey() + "&branch=" + project2Branch.getBranchName().get() | |||
+ "&issues=" + IntStream.range(0, 6).mapToObj(i -> "6_" + i).sorted().collect(joining("%2C"))) | |||
.hasParagraph().hasParagraph() // skip footer | |||
.noMoreBlock(); | |||
} | |||
@DataProvider | |||
public static Object[][] userOrAnalysisChange() { | |||
AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong()); | |||
UserChange userChange = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6), | |||
new Random().nextBoolean() ? null : randomAlphabetic(7))); | |||
return new Object[][] { | |||
{analysisChange}, | |||
{userChange} | |||
}; | |||
} | |||
@DataProvider | |||
public static Object[][] fpOrWontFixValuesByUserOrAnalysisChange() { | |||
AnalysisChange analysisChange = new AnalysisChange(new Random().nextLong()); | |||
UserChange userChange = new UserChange(new Random().nextLong(), new User(randomAlphabetic(5), randomAlphabetic(6), | |||
new Random().nextBoolean() ? null : randomAlphabetic(7))); | |||
return new Object[][] { | |||
{analysisChange, FP}, | |||
{analysisChange, WONT_FIX}, | |||
{userChange, FP}, | |||
{userChange, WONT_FIX} | |||
}; | |||
} | |||
private static ChangedIssue newChangedIssue(String key, Project project, String ruleName) { | |||
return newChangedIssue(key, project, newRule(ruleName)); | |||
} | |||
private static ChangedIssue newChangedIssue(String key, Project project, Rule rule) { | |||
return new ChangedIssue.Builder(key) | |||
.setNewStatus(randomAlphabetic(19)) | |||
.setProject(project) | |||
.setRule(rule) | |||
.build(); | |||
} | |||
private static Rule newRule(String ruleName) { | |||
return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleName); | |||
} | |||
private static Project newProject(String uuid) { | |||
return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").build(); | |||
} | |||
private static Project newBranch(String uuid, String branchName) { | |||
return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").setBranchName(branchName).build(); | |||
} | |||
} |
@@ -1,169 +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 org.junit.Test; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.FieldDiffs; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.user.UserDto; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.db.user.UserTesting.newUserDto; | |||
public class IssueChangeNotificationTest { | |||
private IssueChangeNotification notification = new IssueChangeNotification(); | |||
@Test | |||
public void getProjectKey_returns_null_when_project_is_not_set() { | |||
assertThat(notification.getProjectKey()).isNull(); | |||
} | |||
@Test | |||
public void getChangeAuthor_returns_null_when_issue_is_not_set() { | |||
assertThat(notification.getChangeAuthor()).isNull(); | |||
} | |||
@Test | |||
public void getNewResolution_returns_null_when_issue_is_not_set() { | |||
assertThat(notification.getNewResolution()).isNull(); | |||
} | |||
@Test | |||
public void set_issue() { | |||
UserDto assignee = newUserDto(); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setKey("ABCD") | |||
.setAssigneeUuid(assignee.getUuid()) | |||
.setMessage("Remove this useless method") | |||
.setComponentKey("MyService") | |||
.setCurrentChange(new FieldDiffs().setDiff("resolution", "FALSE-POSITIVE", "FIXED")); | |||
IssueChangeNotification result = notification.setIssue(issue).setAssignee(assignee); | |||
assertThat(result.getFieldValue("key")).isEqualTo("ABCD"); | |||
assertThat(result.getFieldValue("message")).isEqualTo("Remove this useless method"); | |||
assertThat(result.getFieldValue("old.resolution")).isEqualTo("FALSE-POSITIVE"); | |||
assertThat(result.getFieldValue("new.resolution")) | |||
.isEqualTo("FIXED") | |||
.isEqualTo(result.getNewResolution()); | |||
assertThat(result.getFieldValue("assignee")).isEqualTo(assignee.getLogin()); | |||
} | |||
@Test | |||
public void set_issue_with_current_change_having_no_old_value() { | |||
DefaultIssue issue = new DefaultIssue() | |||
.setKey("ABCD") | |||
.setAssigneeUuid("simon") | |||
.setMessage("Remove this useless method") | |||
.setComponentKey("MyService"); | |||
IssueChangeNotification result = notification.setIssue(issue.setCurrentChange(new FieldDiffs().setDiff("resolution", null, "FIXED"))); | |||
assertThat(result.getFieldValue("old.resolution")).isNull(); | |||
assertThat(result.getFieldValue("new.resolution")) | |||
.isEqualTo("FIXED") | |||
.isEqualTo(result.getNewResolution()); | |||
result = notification.setIssue(issue.setCurrentChange(new FieldDiffs().setDiff("resolution", "", "FIXED"))); | |||
assertThat(result.getFieldValue("old.resolution")).isNull(); | |||
assertThat(result.getFieldValue("new.resolution")) | |||
.isEqualTo("FIXED") | |||
.isEqualTo(result.getNewResolution()); | |||
} | |||
@Test | |||
public void set_issue_with_current_change_having_no_new_value() { | |||
DefaultIssue issue = new DefaultIssue() | |||
.setKey("ABCD") | |||
.setAssigneeUuid("simon") | |||
.setMessage("Remove this useless method") | |||
.setComponentKey("MyService"); | |||
IssueChangeNotification result = notification.setIssue(issue.setCurrentChange(new FieldDiffs().setDiff("assignee", "john", null))); | |||
assertThat(result.getFieldValue("old.assignee")).isEqualTo("john"); | |||
assertThat(result.getFieldValue("new.assignee")).isNull(); | |||
assertThat(result.getNewResolution()).isNull(); | |||
result = notification.setIssue(issue.setCurrentChange(new FieldDiffs().setDiff("assignee", "john", ""))); | |||
assertThat(result.getFieldValue("old.assignee")).isEqualTo("john"); | |||
assertThat(result.getFieldValue("new.assignee")).isNull(); | |||
assertThat(result.getNewResolution()).isNull(); | |||
} | |||
@Test | |||
public void set_project_without_branch() { | |||
IssueChangeNotification result = notification.setProject("MyService", "My Service", null, null); | |||
assertThat(result.getFieldValue("projectKey")) | |||
.isEqualTo("MyService") | |||
.isEqualTo(result.getProjectKey()); | |||
assertThat(result.getFieldValue("projectName")).isEqualTo("My Service"); | |||
assertThat(result.getFieldValue("branch")).isNull(); | |||
} | |||
@Test | |||
public void set_project_with_branch() { | |||
IssueChangeNotification result = notification.setProject("MyService", "My Service", "feature1", null); | |||
assertThat(result.getFieldValue("projectKey")) | |||
.isEqualTo("MyService") | |||
.isEqualTo(result.getProjectKey()); | |||
assertThat(result.getFieldValue("projectName")).isEqualTo("My Service"); | |||
assertThat(result.getFieldValue("branch")).isEqualTo("feature1"); | |||
} | |||
@Test | |||
public void set_project_with_pull_request() { | |||
IssueChangeNotification result = notification.setProject("MyService", "My Service", null, "pr-123"); | |||
assertThat(result.getFieldValue("projectKey")) | |||
.isEqualTo("MyService") | |||
.isEqualTo(result.getProjectKey()); | |||
assertThat(result.getFieldValue("projectName")).isEqualTo("My Service"); | |||
assertThat(result.getFieldValue("pullRequest")).isEqualTo("pr-123"); | |||
} | |||
@Test | |||
public void set_component() { | |||
IssueChangeNotification result = notification.setComponent(new ComponentDto().setDbKey("MyService").setLongName("My Service")); | |||
assertThat(result.getFieldValue("componentName")).isEqualTo("My Service"); | |||
assertThat(result.getFieldValue("componentKey")).isEqualTo("MyService"); | |||
} | |||
@Test | |||
public void set_change_author_login() { | |||
UserDto user = newUserDto(); | |||
IssueChangeNotification result = notification.setChangeAuthor(user); | |||
assertThat(result.getFieldValue("changeAuthor")) | |||
.isEqualTo(user.getLogin()) | |||
.isEqualTo(result.getChangeAuthor()); | |||
} | |||
@Test | |||
public void set_rule_name() { | |||
IssueChangeNotification result = notification.setRuleName("Xoo Rule"); | |||
assertThat(result.getFieldValue("ruleName")).isEqualTo("Xoo Rule"); | |||
} | |||
@Test | |||
public void setComment() { | |||
IssueChangeNotification result = notification.setComment("My comment"); | |||
assertThat(result.getFieldValue("comment")).isEqualTo("My comment"); | |||
} | |||
} |
@@ -1,200 +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.io.Resources; | |||
import java.nio.charset.StandardCharsets; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.config.EmailSettings; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.user.UserDto; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.api.CoreProperties.SERVER_BASE_URL; | |||
public class IssueChangesEmailTemplateTest { | |||
@Rule | |||
public DbTester db = DbTester.create(); | |||
private MapSettings settings = new MapSettings().setProperty(SERVER_BASE_URL, "http://nemo.sonarsource.org"); | |||
private IssueChangesEmailTemplate underTest = new IssueChangesEmailTemplate(db.getDbClient(), new EmailSettings(settings.asConfig())); | |||
@Test | |||
public void should_ignore_non_issue_changes() { | |||
Notification notification = new Notification("other"); | |||
EmailMessage message = underTest.format(notification); | |||
assertThat(message).isNull(); | |||
} | |||
@Test | |||
public void email_should_display_assignee_change() throws Exception { | |||
Notification notification = generateNotification() | |||
.setFieldValue("old.assignee", "simon") | |||
.setFieldValue("new.assignee", "louis"); | |||
EmailMessage email = underTest.format(notification); | |||
assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE"); | |||
assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE"); | |||
String message = email.getMessage(); | |||
String expected = Resources.toString(Resources.getResource( | |||
"org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_assignee_change.txt"), | |||
StandardCharsets.UTF_8); | |||
expected = StringUtils.remove(expected, '\r'); | |||
assertThat(message).isEqualTo(expected); | |||
assertThat(email.getFrom()).isNull(); | |||
} | |||
@Test | |||
public void email_should_display_plan_change() throws Exception { | |||
Notification notification = generateNotification() | |||
.setFieldValue("old.actionPlan", null) | |||
.setFieldValue("new.actionPlan", "ABC 1.0"); | |||
EmailMessage email = underTest.format(notification); | |||
assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE"); | |||
assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE"); | |||
String message = email.getMessage(); | |||
String expected = Resources.toString(Resources.getResource( | |||
"org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_action_plan_change.txt"), | |||
StandardCharsets.UTF_8); | |||
expected = StringUtils.remove(expected, '\r'); | |||
assertThat(message).isEqualTo(expected); | |||
assertThat(email.getFrom()).isNull(); | |||
} | |||
@Test | |||
public void email_should_display_resolution_change() throws Exception { | |||
Notification notification = generateNotification() | |||
.setFieldValue("old.resolution", "FALSE-POSITIVE") | |||
.setFieldValue("new.resolution", "FIXED"); | |||
EmailMessage email = underTest.format(notification); | |||
assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE"); | |||
assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE"); | |||
String message = email.getMessage(); | |||
String expected = Resources.toString(Resources.getResource( | |||
"org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_should_display_resolution_change.txt"), | |||
StandardCharsets.UTF_8); | |||
expected = StringUtils.remove(expected, '\r'); | |||
assertThat(message).isEqualTo(expected); | |||
assertThat(email.getFrom()).isNull(); | |||
} | |||
@Test | |||
public void display_component_key_if_no_component_name() throws Exception { | |||
Notification notification = generateNotification() | |||
.setFieldValue("componentName", null); | |||
EmailMessage email = underTest.format(notification); | |||
assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE"); | |||
assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE"); | |||
String message = email.getMessage(); | |||
String expected = Resources.toString(Resources.getResource( | |||
"org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/display_component_key_if_no_component_name.txt"), | |||
StandardCharsets.UTF_8); | |||
expected = StringUtils.remove(expected, '\r'); | |||
assertThat(message).isEqualTo(expected); | |||
} | |||
@Test | |||
public void test_email_with_multiple_changes() throws Exception { | |||
Notification notification = generateNotification() | |||
.setFieldValue("comment", "How to fix it?") | |||
.setFieldValue("old.assignee", "simon") | |||
.setFieldValue("new.assignee", "louis") | |||
.setFieldValue("new.resolution", "FALSE-POSITIVE") | |||
.setFieldValue("new.status", "RESOLVED") | |||
.setFieldValue("new.type", "BUG") | |||
.setFieldValue("new.tags", "bug performance"); | |||
EmailMessage email = underTest.format(notification); | |||
assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE"); | |||
assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE"); | |||
String message = email.getMessage(); | |||
String expected = Resources.toString(Resources.getResource( | |||
"org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_multiple_changes.txt"), StandardCharsets.UTF_8); | |||
expected = StringUtils.remove(expected, '\r'); | |||
assertThat(message).isEqualTo(expected); | |||
assertThat(email.getFrom()).isNull(); | |||
} | |||
@Test | |||
public void test_email_with_issue_on_branch() throws Exception { | |||
Notification notification = generateNotification() | |||
.setFieldValue("branch", "feature1"); | |||
EmailMessage email = underTest.format(notification); | |||
assertThat(email.getMessageId()).isEqualTo("issue-changes/ABCDE"); | |||
assertThat(email.getSubject()).isEqualTo("Struts, change on issue #ABCDE"); | |||
String message = email.getMessage(); | |||
String expected = Resources.toString(Resources.getResource( | |||
"org/sonar/server/issue/notification/IssueChangesEmailTemplateTest/email_with_issue_on_branch.txt"), | |||
StandardCharsets.UTF_8); | |||
expected = StringUtils.remove(expected, '\r'); | |||
assertThat(message).isEqualTo(expected); | |||
} | |||
@Test | |||
public void notification_sender_should_be_the_author_of_change() { | |||
UserDto user = db.users().insertUser(); | |||
Notification notification = new IssueChangeNotification() | |||
.setChangeAuthor(user) | |||
.setProject("Struts", "org.apache:struts", null, null); | |||
EmailMessage message = underTest.format(notification); | |||
assertThat(message.getFrom()).isEqualTo(user.getName()); | |||
} | |||
@Test | |||
public void notification_contains_user_login_when_user_is_removed() { | |||
UserDto user = db.users().insertDisabledUser(); | |||
Notification notification = new IssueChangeNotification() | |||
.setChangeAuthor(user) | |||
.setProject("Struts", "org.apache:struts", null, null); | |||
EmailMessage message = underTest.format(notification); | |||
assertThat(message.getFrom()).isEqualTo(user.getLogin()); | |||
} | |||
private static Notification generateNotification() { | |||
return new IssueChangeNotification() | |||
.setFieldValue("projectName", "Struts") | |||
.setFieldValue("projectKey", "org.apache:struts") | |||
.setFieldValue("componentName", "Action") | |||
.setFieldValue("componentKey", "org.apache.struts.Action") | |||
.setFieldValue("key", "ABCDE") | |||
.setFieldValue("ruleName", "Avoid Cycles") | |||
.setFieldValue("message", "Has 3 cycles"); | |||
} | |||
} |
@@ -0,0 +1,110 @@ | |||
/* | |||
* 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 java.util.Random; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.component.BranchDto; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.AnalysisChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; | |||
public class IssuesChangesNotificationBuilderTesting { | |||
public static Rule ruleOf(RuleDto rule) { | |||
return new Rule(rule.getKey(), rule.getName()); | |||
} | |||
public static Rule ruleOf(RuleDefinitionDto rule) { | |||
return new Rule(rule.getKey(), rule.getName()); | |||
} | |||
public static User userOf(UserDto changeAuthor) { | |||
return new User(changeAuthor.getUuid(), changeAuthor.getLogin(), changeAuthor.getName()); | |||
} | |||
public static Project projectBranchOf(DbTester db, ComponentDto branch) { | |||
BranchDto branchDto = db.getDbClient().branchDao().selectByUuid(db.getSession(), branch.uuid()).get(); | |||
checkArgument(!branchDto.isMain(), "should be a branch"); | |||
return new Project.Builder(branch.uuid()) | |||
.setKey(branch.getKey()) | |||
.setProjectName(branch.name()) | |||
.setBranchName(branchDto.getKey()) | |||
.build(); | |||
} | |||
public static Project projectOf(ComponentDto project) { | |||
return new Project.Builder(project.uuid()) | |||
.setKey(project.getKey()) | |||
.setProjectName(project.name()) | |||
.build(); | |||
} | |||
static ChangedIssue newChangedIssue(String key, Project project, Rule rule) { | |||
return new ChangedIssue.Builder(key) | |||
.setNewStatus(randomAlphabetic(19)) | |||
.setProject(project) | |||
.setRule(rule) | |||
.build(); | |||
} | |||
static ChangedIssue newChangedIssue(String key, String status, Project project, String ruleName) { | |||
return newChangedIssue(key, status, project, newRule(ruleName)); | |||
} | |||
static ChangedIssue newChangedIssue(String key, String status, Project project, Rule rule) { | |||
return new ChangedIssue.Builder(key) | |||
.setNewStatus(status) | |||
.setProject(project) | |||
.setRule(rule) | |||
.build(); | |||
} | |||
static Rule newRule(String ruleName) { | |||
return new Rule(RuleKey.of(randomAlphabetic(6), randomAlphabetic(7)), ruleName); | |||
} | |||
static Project newProject(String uuid) { | |||
return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").build(); | |||
} | |||
static Project newBranch(String uuid, String branchName) { | |||
return new Project.Builder(uuid).setProjectName(uuid + "_name").setKey(uuid + "_key").setBranchName(branchName).build(); | |||
} | |||
static UserChange newUserChange() { | |||
return new UserChange(new Random().nextLong(), new User(randomAlphabetic(4), randomAlphabetic(5), randomAlphabetic(6))); | |||
} | |||
static AnalysisChange newAnalysisChange() { | |||
return new AnalysisChange(new Random().nextLong()); | |||
} | |||
} |
@@ -0,0 +1,37 @@ | |||
/* | |||
* 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 org.junit.Test; | |||
import org.sonar.core.platform.ComponentContainer; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; | |||
public class IssuesChangesNotificationModuleTest { | |||
@Test | |||
public void verify_count_of_added_components() { | |||
ComponentContainer container = new ComponentContainer(); | |||
new IssuesChangesNotificationModule().configure(container); | |||
assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 7); | |||
} | |||
} |
@@ -0,0 +1,35 @@ | |||
/* | |||
* 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 org.junit.Test; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class IssuesChangesNotificationTest { | |||
private IssuesChangesNotification notification = new IssuesChangesNotification(); | |||
@Test | |||
public void verify_type() { | |||
assertThat(notification.getType()).isEqualTo("issues-changes"); | |||
} | |||
} |
@@ -35,7 +35,13 @@ import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.issue.notification.IssueChangeNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Rule; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.issue.ws.SearchResponseData; | |||
import org.sonar.server.notification.NotificationManager; | |||
@@ -51,27 +57,25 @@ public class IssueUpdater { | |||
private final WebIssueStorage issueStorage; | |||
private final NotificationManager notificationService; | |||
private final IssueChangePostProcessor issueChangePostProcessor; | |||
private final IssuesChangesNotificationSerializer notificationSerializer; | |||
public IssueUpdater(DbClient dbClient, WebIssueStorage issueStorage, NotificationManager notificationService, | |||
IssueChangePostProcessor issueChangePostProcessor) { | |||
IssueChangePostProcessor issueChangePostProcessor, IssuesChangesNotificationSerializer notificationSerializer) { | |||
this.dbClient = dbClient; | |||
this.issueStorage = issueStorage; | |||
this.notificationService = notificationService; | |||
this.issueChangePostProcessor = issueChangePostProcessor; | |||
this.notificationSerializer = notificationSerializer; | |||
} | |||
/** | |||
* Same as {@link #saveIssue(DbSession, DefaultIssue, IssueChangeContext, String)} but populates the specified | |||
* {@link SearchResponseData} with the DTOs (rule and components) retrieved from DB to save the issue. | |||
*/ | |||
public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context, | |||
@Nullable String comment, boolean refreshMeasures) { | |||
public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, | |||
IssueChangeContext context, boolean refreshMeasures) { | |||
Optional<RuleDefinitionDto> rule = getRuleByKey(dbSession, issue.getRuleKey()); | |||
ComponentDto project = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid()); | |||
BranchDto branch = getBranch(dbSession, issue, issue.projectUuid()); | |||
ComponentDto component = getComponent(dbSession, issue, issue.componentUuid()); | |||
IssueDto issueDto = doSaveIssue(dbSession, issue, context, comment, rule, project, branch, component); | |||
IssueDto issueDto = doSaveIssue(dbSession, issue, context, rule, project, branch); | |||
SearchResponseData result = new SearchResponseData(issueDto); | |||
rule.ifPresent(r -> result.addRules(singletonList(r))); | |||
@@ -86,31 +90,38 @@ public class IssueUpdater { | |||
return result; | |||
} | |||
public IssueDto saveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment) { | |||
Optional<RuleDefinitionDto> rule = getRuleByKey(session, issue.getRuleKey()); | |||
ComponentDto project = getComponent(session, issue, issue.projectUuid()); | |||
BranchDto branch = getBranch(session, issue, issue.projectUuid()); | |||
ComponentDto component = getComponent(session, issue, issue.componentUuid()); | |||
return doSaveIssue(session, issue, context, comment, rule, project, branch, component); | |||
} | |||
private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment, | |||
Optional<RuleDefinitionDto> rule, ComponentDto project, BranchDto branch, ComponentDto component) { | |||
private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, | |||
Optional<RuleDefinitionDto> rule, ComponentDto project, BranchDto branchDto) { | |||
IssueDto issueDto = issueStorage.save(session, singletonList(issue)).iterator().next(); | |||
if (issue.type() != RuleType.SECURITY_HOTSPOT && hasNotificationSupport(branch)) { | |||
String assigneeUuid = issue.assignee(); | |||
UserDto assignee = assigneeUuid == null ? null : dbClient.userDao().selectByUuid(session, assigneeUuid); | |||
String authorUuid = context.userUuid(); | |||
UserDto author = authorUuid == null ? null : dbClient.userDao().selectByUuid(session, authorUuid); | |||
notificationService.scheduleForSending(new IssueChangeNotification() | |||
.setIssue(issue) | |||
.setAssignee(assignee) | |||
.setChangeAuthor(author) | |||
.setRuleName(rule.map(RuleDefinitionDto::getName).orElse(null)) | |||
.setProject(project) | |||
.setComponent(component) | |||
.setComment(comment)); | |||
if (issue.type() == RuleType.SECURITY_HOTSPOT | |||
// since this method is called after an update of the issue, date should never be null | |||
|| issue.updateDate() == null | |||
// name of rule is displayed in notification, rule must therefor be present | |||
|| !rule.isPresent() | |||
// notification are not supported on PRs and short lived branches | |||
|| !hasNotificationSupport(branchDto)) { | |||
return issueDto; | |||
} | |||
Optional<UserDto> assignee = Optional.ofNullable(issue.assignee()) | |||
.map(assigneeUuid -> dbClient.userDao().selectByUuid(session, assigneeUuid)); | |||
UserDto author = Optional.ofNullable(context.userUuid()) | |||
.map(authorUuid -> dbClient.userDao().selectByUuid(session, authorUuid)) | |||
.orElseThrow(() -> new IllegalStateException("Can not find dto for change author " + context.userUuid())); | |||
IssuesChangesNotificationBuilder notificationBuilder = new IssuesChangesNotificationBuilder(singleton( | |||
new ChangedIssue.Builder(issue.key()) | |||
.setNewResolution(issue.resolution()) | |||
.setNewStatus(issue.status()) | |||
.setAssignee(assignee.map(assigneeDto -> new User(assigneeDto.getUuid(), assigneeDto.getLogin(), assigneeDto.getName())).orElse(null)) | |||
.setRule(rule.map(r -> new Rule(r.getKey(), r.getName())).get()) | |||
.setProject(new Project.Builder(project.uuid()) | |||
.setKey(project.getKey()) | |||
.setProjectName(project.name()) | |||
.setBranchName(branchDto.isMain() ? null : branchDto.getKey()) | |||
.build()) | |||
.build()), | |||
new UserChange(issue.updateDate().getTime(), new User(author.getUuid(), author.getLogin(), author.getName()))); | |||
notificationService.scheduleForSending(notificationSerializer.serialize(notificationBuilder)); | |||
return issueDto; | |||
} | |||
@@ -98,7 +98,7 @@ public class AddCommentAction implements IssuesWsAction { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid()); | |||
DefaultIssue defaultIssue = issueDto.toDefaultIssue(); | |||
issueFieldsSetter.addComment(defaultIssue, wsRequest.getText(), context); | |||
SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context, wsRequest.getText(), false); | |||
SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context, false); | |||
responseWriter.write(defaultIssue.key(), preloadedSearchResponseData, request, response); | |||
} | |||
} |
@@ -116,7 +116,7 @@ public class AssignAction implements IssuesWsAction { | |||
} | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid()); | |||
if (issueFieldsSetter.assign(issue, user, context)) { | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issue, context, null, false); | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issue, context, false); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -30,6 +30,8 @@ import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Consumer; | |||
import java.util.function.Predicate; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.issue.DefaultTransitions; | |||
import org.sonar.api.rule.RuleKey; | |||
@@ -60,7 +62,12 @@ import org.sonar.server.issue.AssignAction; | |||
import org.sonar.server.issue.IssueChangePostProcessor; | |||
import org.sonar.server.issue.RemoveTagsAction; | |||
import org.sonar.server.issue.WebIssueStorage; | |||
import org.sonar.server.issue.notification.IssueChangeNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.Project; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.User; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Issues; | |||
@@ -72,12 +79,12 @@ import static java.lang.String.format; | |||
import static java.util.Objects.requireNonNull; | |||
import static java.util.function.Function.identity; | |||
import static java.util.stream.Collectors.toMap; | |||
import static java.util.stream.Collectors.toSet; | |||
import static org.sonar.api.issue.DefaultTransitions.REOPEN; | |||
import static org.sonar.api.rule.Severity.BLOCKER; | |||
import static org.sonar.api.rules.RuleType.BUG; | |||
import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01; | |||
import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
import static org.sonar.server.es.SearchOptions.MAX_LIMIT; | |||
import static org.sonar.server.issue.AbstractChangeTagsAction.TAGS_PARAMETER; | |||
@@ -113,10 +120,11 @@ public class BulkChangeAction implements IssuesWsAction { | |||
private final NotificationManager notificationService; | |||
private final List<Action> actions; | |||
private final IssueChangePostProcessor issueChangePostProcessor; | |||
private final IssuesChangesNotificationSerializer notificationSerializer; | |||
public BulkChangeAction(System2 system2, UserSession userSession, DbClient dbClient, WebIssueStorage issueStorage, | |||
NotificationManager notificationService, List<Action> actions, | |||
IssueChangePostProcessor issueChangePostProcessor) { | |||
IssueChangePostProcessor issueChangePostProcessor, IssuesChangesNotificationSerializer notificationSerializer) { | |||
this.system2 = system2; | |||
this.userSession = userSession; | |||
this.dbClient = dbClient; | |||
@@ -124,6 +132,7 @@ public class BulkChangeAction implements IssuesWsAction { | |||
this.notificationService = notificationService; | |||
this.actions = actions; | |||
this.issueChangePostProcessor = issueChangePostProcessor; | |||
this.notificationSerializer = notificationSerializer; | |||
} | |||
@Override | |||
@@ -200,12 +209,12 @@ public class BulkChangeAction implements IssuesWsAction { | |||
refreshLiveMeasures(dbSession, bulkChangeData, result); | |||
Set<String> assigneeUuids = items.stream().map(DefaultIssue::assignee).filter(Objects::nonNull).collect(toSet()); | |||
Set<String> assigneeUuids = items.stream().map(DefaultIssue::assignee).filter(Objects::nonNull).collect(Collectors.toSet()); | |||
Map<String, UserDto> userDtoByUuid = dbClient.userDao().selectByUuids(dbSession, assigneeUuids).stream().collect(toMap(UserDto::getUuid, u -> u)); | |||
String authorUuid = requireNonNull(userSession.getUuid(), "User uuid cannot be null"); | |||
UserDto author = dbClient.userDao().selectByUuid(dbSession, authorUuid); | |||
checkState(author != null, "User with uuid '%s' does not exist"); | |||
items.forEach(sendNotification(bulkChangeData, userDtoByUuid, author)); | |||
sendNotification(items, bulkChangeData, userDtoByUuid, author); | |||
return result; | |||
} | |||
@@ -216,7 +225,7 @@ public class BulkChangeAction implements IssuesWsAction { | |||
} | |||
Set<String> touchedComponentUuids = result.success.stream() | |||
.map(DefaultIssue::componentUuid) | |||
.collect(toSet()); | |||
.collect(Collectors.toSet()); | |||
List<ComponentDto> touchedComponents = touchedComponentUuids.stream() | |||
.map(data.componentsByUuid::get) | |||
.collect(MoreCollectors.toList(touchedComponentUuids.size())); | |||
@@ -253,27 +262,70 @@ public class BulkChangeAction implements IssuesWsAction { | |||
bulkChangeData.getCommentAction().ifPresent(action -> action.execute(bulkChangeData.getProperties(action.key()), actionContext)); | |||
} | |||
private Consumer<DefaultIssue> sendNotification(BulkChangeData bulkChangeData, Map<String, UserDto> userDtoByUuid, UserDto author) { | |||
return issue -> { | |||
if (bulkChangeData.sendNotification && issue.type() != RuleType.SECURITY_HOTSPOT) { | |||
BranchDto branch = bulkChangeData.branchesByProjectUuid.get(issue.projectUuid()); | |||
if (hasNotificationSupport(branch)) { | |||
notificationService.scheduleForSending(new IssueChangeNotification() | |||
.setIssue(issue) | |||
.setAssignee(userDtoByUuid.get(issue.assignee())) | |||
.setChangeAuthor(author) | |||
.setRuleName(bulkChangeData.rulesByKey.get(issue.ruleKey()).getName()) | |||
.setProject(bulkChangeData.projectsByUuid.get(issue.projectUuid())) | |||
.setComponent(bulkChangeData.componentsByUuid.get(issue.componentUuid()))); | |||
} | |||
} | |||
}; | |||
private void sendNotification(Collection<DefaultIssue> issues, BulkChangeData bulkChangeData, Map<String, UserDto> userDtoByUuid, UserDto author) { | |||
if (!bulkChangeData.sendNotification) { | |||
return; | |||
} | |||
Set<ChangedIssue> changedIssues = issues.stream() | |||
.filter(issue -> issue.type() != RuleType.SECURITY_HOTSPOT) | |||
// should not happen but filter it out anyway to avoid NPE in oldestUpdateDate call below | |||
.filter(issue -> issue.updateDate() != null) | |||
.map(issue -> toNotification(bulkChangeData, userDtoByUuid, issue)) | |||
.filter(Objects::nonNull) | |||
.collect(toSet(issues.size())); | |||
if (changedIssues.isEmpty()) { | |||
return; | |||
} | |||
IssuesChangesNotificationBuilder builder = new IssuesChangesNotificationBuilder( | |||
changedIssues, | |||
new UserChange(oldestUpdateDate(issues), new User(author.getUuid(), author.getLogin(), author.getName()))); | |||
notificationService.scheduleForSending(notificationSerializer.serialize(builder)); | |||
} | |||
@CheckForNull | |||
private ChangedIssue toNotification(BulkChangeData bulkChangeData, Map<String, UserDto> userDtoByUuid, DefaultIssue issue) { | |||
BranchDto branchDto = bulkChangeData.branchesByProjectUuid.get(issue.projectUuid()); | |||
if (!hasNotificationSupport(branchDto)) { | |||
return null; | |||
} | |||
RuleDefinitionDto ruleDefinitionDto = bulkChangeData.rulesByKey.get(issue.ruleKey()); | |||
ComponentDto projectDto = bulkChangeData.projectsByUuid.get(issue.projectUuid()); | |||
if (ruleDefinitionDto == null || projectDto == null) { | |||
return null; | |||
} | |||
Optional<UserDto> assignee = Optional.ofNullable(issue.assignee()).map(userDtoByUuid::get); | |||
return new ChangedIssue.Builder(issue.key()) | |||
.setNewStatus(issue.status()) | |||
.setNewResolution(issue.resolution()) | |||
.setAssignee(assignee.map(u -> new User(u.getUuid(), u.getLogin(), u.getName())).orElse(null)) | |||
.setRule(new IssuesChangesNotificationBuilder.Rule(ruleDefinitionDto.getKey(), ruleDefinitionDto.getName())) | |||
.setProject(new Project.Builder(projectDto.uuid()) | |||
.setKey(projectDto.getKey()) | |||
.setProjectName(projectDto.name()) | |||
.setBranchName(branchDto.isMain() ? null : branchDto.getKey()) | |||
.build()) | |||
.build(); | |||
} | |||
private static boolean hasNotificationSupport(@Nullable BranchDto branch) { | |||
return branch != null && branch.getBranchType() != BranchType.PULL_REQUEST && branch.getBranchType() != BranchType.SHORT; | |||
} | |||
private static long oldestUpdateDate(Collection<DefaultIssue> issues) { | |||
long res = Long.MAX_VALUE; | |||
for (DefaultIssue issue : issues) { | |||
long issueUpdateDate = issue.updateDate().getTime(); | |||
if (issueUpdateDate < res) { | |||
res = issueUpdateDate; | |||
} | |||
} | |||
return res; | |||
} | |||
private static Issues.BulkChangeWsResponse toWsResponse(BulkChangeResult result) { | |||
return Issues.BulkChangeWsResponse.newBuilder() | |||
.setTotal(result.countTotal()) |
@@ -104,7 +104,7 @@ public class DoTransitionAction implements IssuesWsAction { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid()); | |||
transitionService.checkTransitionPermission(transitionKey, defaultIssue); | |||
if (transitionService.doTransition(defaultIssue, context, transitionKey)) { | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null, true); | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, true); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -107,7 +107,7 @@ public class SetSeverityAction implements IssuesWsAction { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getUuid()); | |||
if (issueFieldsSetter.setManualSeverity(issue, severity, context)) { | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, true); | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -105,7 +105,7 @@ public class SetTagsAction implements IssuesWsAction { | |||
DefaultIssue issue = issueDto.toDefaultIssue(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getUuid()); | |||
if (issueFieldsSetter.setTags(issue, tags, context)) { | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, false); | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, false); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -113,7 +113,7 @@ public class SetTypeAction implements IssuesWsAction { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getUuid()); | |||
if (issueFieldsSetter.setType(issue, ruleType, context)) { | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, true); | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, true); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -79,9 +79,7 @@ import org.sonar.server.issue.TransitionAction; | |||
import org.sonar.server.issue.index.IssueIndexDefinition; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.ChangesOnMyIssueNotificationHandler; | |||
import org.sonar.server.issue.notification.DoNotFixNotificationHandler; | |||
import org.sonar.server.issue.notification.IssueChangesEmailTemplate; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationModule; | |||
import org.sonar.server.issue.notification.MyNewIssuesEmailTemplate; | |||
import org.sonar.server.issue.notification.MyNewIssuesNotificationHandler; | |||
import org.sonar.server.issue.notification.NewIssuesEmailTemplate; | |||
@@ -151,10 +149,10 @@ import org.sonar.server.property.InternalPropertiesImpl; | |||
import org.sonar.server.property.ws.PropertiesWs; | |||
import org.sonar.server.qualitygate.QualityGateModule; | |||
import org.sonar.server.qualitygate.notification.QGChangeNotificationHandler; | |||
import org.sonar.server.qualityprofile.BuiltInQProfileDefinitionsBridge; | |||
import org.sonar.server.qualityprofile.BuiltInQProfileRepositoryImpl; | |||
import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationHandler; | |||
import org.sonar.server.qualityprofile.BuiltInQPChangeNotificationTemplate; | |||
import org.sonar.server.qualityprofile.BuiltInQProfileDefinitionsBridge; | |||
import org.sonar.server.qualityprofile.BuiltInQProfileRepositoryImpl; | |||
import org.sonar.server.qualityprofile.QProfileBackuperImpl; | |||
import org.sonar.server.qualityprofile.QProfileComparison; | |||
import org.sonar.server.qualityprofile.QProfileCopier; | |||
@@ -408,15 +406,11 @@ public class PlatformLevel4 extends PlatformLevel { | |||
IssueWsModule.class, | |||
NewIssuesEmailTemplate.class, | |||
MyNewIssuesEmailTemplate.class, | |||
IssueChangesEmailTemplate.class, | |||
ChangesOnMyIssueNotificationHandler.class, | |||
ChangesOnMyIssueNotificationHandler.newMetadata(), | |||
IssuesChangesNotificationModule.class, | |||
NewIssuesNotificationHandler.class, | |||
NewIssuesNotificationHandler.newMetadata(), | |||
MyNewIssuesNotificationHandler.class, | |||
MyNewIssuesNotificationHandler.newMetadata(), | |||
DoNotFixNotificationHandler.class, | |||
DoNotFixNotificationHandler.newMetadata(), | |||
// Security reports | |||
SecurityReportsWsModule.class, |
@@ -23,6 +23,7 @@ import java.io.UnsupportedEncodingException; | |||
import java.net.URLEncoder; | |||
import java.util.Comparator; | |||
import java.util.Date; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.notifications.Notification; | |||
import org.sonar.api.platform.Server; | |||
import org.sonar.server.issue.notification.EmailMessage; | |||
@@ -42,6 +43,7 @@ public class BuiltInQPChangeNotificationTemplate implements EmailTemplate { | |||
} | |||
@Override | |||
@CheckForNull | |||
public EmailMessage format(Notification notification) { | |||
if (!BuiltInQPChangeNotification.TYPE.equals(notification.getType())) { | |||
return null; | |||
@@ -94,7 +96,7 @@ public class BuiltInQPChangeNotificationTemplate implements EmailTemplate { | |||
return new EmailMessage() | |||
.setMessageId(BuiltInQPChangeNotification.TYPE) | |||
.setSubject("Built-in quality profiles have been updated") | |||
.setMessage(message.toString()); | |||
.setPlainTextMessage(message.toString()); | |||
} | |||
private static String plural(int count) { |
@@ -41,7 +41,11 @@ import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.IssueChangeNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.issue.ws.SearchResponseData; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
@@ -55,10 +59,15 @@ import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.never; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.verifyZeroInteractions; | |||
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; | |||
import static org.sonar.api.rule.Severity.BLOCKER; | |||
import static org.sonar.api.rule.Severity.MAJOR; | |||
import static org.sonar.db.component.BranchType.LONG; | |||
import static org.sonar.db.component.ComponentTesting.newFileDto; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.projectBranchOf; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.projectOf; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.ruleOf; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.userOf; | |||
public class IssueUpdaterTest { | |||
@@ -78,27 +87,29 @@ public class IssueUpdaterTest { | |||
private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter(); | |||
private NotificationManager notificationManager = mock(NotificationManager.class); | |||
private ArgumentCaptor<IssueChangeNotification> notificationArgumentCaptor = ArgumentCaptor.forClass(IssueChangeNotification.class); | |||
private ArgumentCaptor<IssuesChangesNotification> notificationArgumentCaptor = ArgumentCaptor.forClass(IssuesChangesNotification.class); | |||
private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); | |||
private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); | |||
private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer(); | |||
private IssueUpdater underTest = new IssueUpdater(dbClient, | |||
new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer), notificationManager, issueChangePostProcessor); | |||
new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer), notificationManager, issueChangePostProcessor, issuesChangesSerializer); | |||
@Test | |||
public void update_issue() { | |||
DefaultIssue issue = db.issues().insertIssue(i -> i.setSeverity(MAJOR)).toDefaultIssue(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), "user_uuid"); | |||
UserDto user = db.users().insertUser(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), user.getUuid()); | |||
issueFieldsSetter.setSeverity(issue, BLOCKER, context); | |||
underTest.saveIssue(db.getSession(), issue, context, null); | |||
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
IssueDto issueReloaded = dbClient.issueDao().selectByKey(db.getSession(), issue.key()).get(); | |||
assertThat(issueReloaded.getSeverity()).isEqualTo(BLOCKER); | |||
} | |||
@Test | |||
public void verify_notification() { | |||
public void verify_notification_without_resolution() { | |||
UserDto assignee = db.users().insertUser(); | |||
RuleDto rule = db.rules().insertRule(); | |||
ComponentDto project = db.components().insertMainBranch(); | |||
@@ -113,21 +124,52 @@ public class IssueUpdaterTest { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), changeAuthor.getUuid()); | |||
issueFieldsSetter.setSeverity(issue, BLOCKER, context); | |||
underTest.saveIssue(db.getSession(), issue, context, "increase severity"); | |||
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture()); | |||
IssueChangeNotification issueChangeNotification = notificationArgumentCaptor.getValue(); | |||
assertThat(issueChangeNotification.getFieldValue("key")).isEqualTo(issue.key()); | |||
assertThat(issueChangeNotification.getFieldValue("old.severity")).isEqualTo(MAJOR); | |||
assertThat(issueChangeNotification.getFieldValue("new.severity")).isEqualTo(BLOCKER); | |||
assertThat(issueChangeNotification.getFieldValue("componentKey")).isEqualTo(file.getDbKey()); | |||
assertThat(issueChangeNotification.getFieldValue("componentName")).isEqualTo(file.longName()); | |||
assertThat(issueChangeNotification.getFieldValue("projectKey")).isEqualTo(project.getDbKey()); | |||
assertThat(issueChangeNotification.getFieldValue("projectName")).isEqualTo(project.name()); | |||
assertThat(issueChangeNotification.getFieldValue("ruleName")).isEqualTo(rule.getName()); | |||
assertThat(issueChangeNotification.getFieldValue("changeAuthor")).isEqualTo(changeAuthor.getLogin()); | |||
assertThat(issueChangeNotification.getFieldValue("comment")).isEqualTo("increase severity"); | |||
assertThat(issueChangeNotification.getFieldValue("assignee")).isEqualTo(assignee.getLogin()); | |||
IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue(); | |||
IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotification); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changedIssue = builder.getIssues().iterator().next(); | |||
assertThat(changedIssue.getKey()).isEqualTo(issue.key()); | |||
assertThat(changedIssue.getNewStatus()).isEqualTo(issue.status()); | |||
assertThat(changedIssue.getNewResolution()).isEmpty(); | |||
assertThat(changedIssue.getAssignee()).contains(userOf(assignee)); | |||
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule)); | |||
assertThat(changedIssue.getProject()).isEqualTo(projectOf(project)); | |||
assertThat(builder.getChange()).isEqualTo(new UserChange(issue.updateDate().getTime(), userOf(changeAuthor))); | |||
} | |||
@Test | |||
public void verify_notification_with_resolution() { | |||
UserDto assignee = db.users().insertUser(); | |||
RuleDto rule = db.rules().insertRule(); | |||
ComponentDto project = db.components().insertMainBranch(); | |||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||
RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)]; | |||
DefaultIssue issue = db.issues().insertIssue(IssueTesting.newIssue(rule.getDefinition(), project, file) | |||
.setType(randomTypeExceptHotspot)) | |||
.setSeverity(MAJOR) | |||
.setAssigneeUuid(assignee.getUuid()) | |||
.toDefaultIssue(); | |||
UserDto changeAuthor = db.users().insertUser(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), changeAuthor.getUuid()); | |||
issueFieldsSetter.setResolution(issue, RESOLUTION_FIXED, context); | |||
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture()); | |||
IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue(); | |||
IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotification); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changedIssue = builder.getIssues().iterator().next(); | |||
assertThat(changedIssue.getKey()).isEqualTo(issue.key()); | |||
assertThat(changedIssue.getNewStatus()).isEqualTo(issue.status()); | |||
assertThat(changedIssue.getNewResolution()).contains(RESOLUTION_FIXED); | |||
assertThat(changedIssue.getAssignee()).contains(userOf(assignee)); | |||
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule)); | |||
assertThat(changedIssue.getProject()).isEqualTo(projectOf(project)); | |||
assertThat(builder.getChange()).isEqualTo(new UserChange(issue.updateDate().getTime(), userOf(changeAuthor))); | |||
} | |||
@Test | |||
@@ -145,7 +187,7 @@ public class IssueUpdaterTest { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), changeAuthor.getUuid()); | |||
issueFieldsSetter.setSeverity(issue, BLOCKER, context); | |||
underTest.saveIssue(db.getSession(), issue, context, "increase severity"); | |||
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
verify(notificationManager, never()).scheduleForSending(any()); | |||
} | |||
@@ -159,17 +201,24 @@ public class IssueUpdaterTest { | |||
RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)]; | |||
DefaultIssue issue = db.issues().insertIssue(IssueTesting.newIssue(rule.getDefinition(), branch, file) | |||
.setType(randomTypeExceptHotspot)).setSeverity(MAJOR).toDefaultIssue(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), "user_uuid"); | |||
UserDto changeAuthor = db.users().insertUser(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), changeAuthor.getUuid()); | |||
issueFieldsSetter.setSeverity(issue, BLOCKER, context); | |||
underTest.saveIssue(db.getSession(), issue, context, "increase severity"); | |||
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture()); | |||
IssueChangeNotification issueChangeNotification = notificationArgumentCaptor.getValue(); | |||
assertThat(issueChangeNotification.getFieldValue("key")).isEqualTo(issue.key()); | |||
assertThat(issueChangeNotification.getFieldValue("projectKey")).isEqualTo(project.getDbKey()); | |||
assertThat(issueChangeNotification.getFieldValue("projectName")).isEqualTo(project.name()); | |||
assertThat(issueChangeNotification.getFieldValue("branch")).isEqualTo(branch.getBranch()); | |||
IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue(); | |||
IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotification); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changedIssue = builder.getIssues().iterator().next(); | |||
assertThat(changedIssue.getKey()).isEqualTo(issue.key()); | |||
assertThat(changedIssue.getNewStatus()).isEqualTo(issue.status()); | |||
assertThat(changedIssue.getNewResolution()).isEmpty(); | |||
assertThat(changedIssue.getAssignee()).isEmpty(); | |||
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule)); | |||
assertThat(changedIssue.getProject()).isEqualTo(projectBranchOf(db, branch)); | |||
assertThat(builder.getChange()).isEqualTo(new UserChange(issue.updateDate().getTime(), userOf(changeAuthor))); | |||
} | |||
@Test | |||
@@ -184,7 +233,7 @@ public class IssueUpdaterTest { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), "user_uuid"); | |||
issueFieldsSetter.setSeverity(issue, BLOCKER, context); | |||
underTest.saveIssue(db.getSession(), issue, context, "increase severity"); | |||
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
verifyZeroInteractions(notificationManager); | |||
} | |||
@@ -201,7 +250,7 @@ public class IssueUpdaterTest { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), "user_uuid"); | |||
issueFieldsSetter.setSeverity(issue, BLOCKER, context); | |||
underTest.saveIssue(db.getSession(), issue, context, "increase severity"); | |||
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
verifyZeroInteractions(notificationManager); | |||
} | |||
@@ -213,14 +262,13 @@ public class IssueUpdaterTest { | |||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||
RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)]; | |||
DefaultIssue issue = db.issues().insertIssue(IssueTesting.newIssue(rule.getDefinition(), project, file) | |||
.setType(randomTypeExceptHotspot)).setSeverity(MAJOR).toDefaultIssue(); | |||
.setType(randomTypeExceptHotspot)).setSeverity(MAJOR).toDefaultIssue(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), "user_uuid"); | |||
issueFieldsSetter.setSeverity(issue, BLOCKER, context); | |||
underTest.saveIssue(db.getSession(), issue, context, null); | |||
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture()); | |||
assertThat(notificationArgumentCaptor.getValue().getFieldValue("ruleName")).isNull(); | |||
verifyZeroInteractions(notificationManager); | |||
} | |||
@Test | |||
@@ -231,7 +279,7 @@ public class IssueUpdaterTest { | |||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||
RuleType randomTypeExceptHotspot = RuleType.values()[nextInt(RuleType.values().length - 1)]; | |||
DefaultIssue issue = db.issues().insertIssue(IssueTesting.newIssue(rule.getDefinition(), project, file) | |||
.setType(randomTypeExceptHotspot)) | |||
.setType(randomTypeExceptHotspot)) | |||
.setAssigneeUuid(oldAssignee.getUuid()) | |||
.toDefaultIssue(); | |||
UserDto changeAuthor = db.users().insertUser(); | |||
@@ -239,14 +287,20 @@ public class IssueUpdaterTest { | |||
UserDto newAssignee = db.users().insertUser(); | |||
issueFieldsSetter.assign(issue, newAssignee, context); | |||
underTest.saveIssue(db.getSession(), issue, context, null); | |||
underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
verify(notificationManager).scheduleForSending(notificationArgumentCaptor.capture()); | |||
IssueChangeNotification issueChangeNotification = notificationArgumentCaptor.getValue(); | |||
assertThat(issueChangeNotification.getFieldValue("key")).isEqualTo(issue.key()); | |||
assertThat(issueChangeNotification.getFieldValue("new.assignee")).isEqualTo(newAssignee.getName()); | |||
assertThat(issueChangeNotification.getFieldValue("old.assignee")).isNull(); | |||
assertThat(issueChangeNotification.getFieldValue("assignee")).isEqualTo(newAssignee.getLogin()); | |||
IssuesChangesNotification issueChangeNotification = notificationArgumentCaptor.getValue(); | |||
IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotification); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changedIssue = builder.getIssues().iterator().next(); | |||
assertThat(changedIssue.getKey()).isEqualTo(issue.key()); | |||
assertThat(changedIssue.getNewStatus()).isEqualTo(issue.status()); | |||
assertThat(changedIssue.getNewResolution()).isEmpty(); | |||
assertThat(changedIssue.getAssignee()).contains(userOf(newAssignee)); | |||
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule)); | |||
assertThat(changedIssue.getProject()).isEqualTo(projectOf(project)); | |||
assertThat(builder.getChange()).isEqualTo(new UserChange(issue.updateDate().getTime(), userOf(changeAuthor))); | |||
} | |||
@Test | |||
@@ -256,10 +310,11 @@ public class IssueUpdaterTest { | |||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||
IssueDto issueDto = IssueTesting.newIssue(rule.getDefinition(), project, file); | |||
DefaultIssue issue = db.issues().insertIssue(issueDto).setSeverity(MAJOR).toDefaultIssue(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), "user_uuid"); | |||
UserDto changeAuthor = db.users().insertUser(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), changeAuthor.getUuid()); | |||
issueFieldsSetter.setSeverity(issue, BLOCKER, context); | |||
SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, null, true); | |||
SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, true); | |||
assertThat(preloadedSearchResponseData.getIssues()) | |||
.hasSize(1); | |||
@@ -284,7 +339,7 @@ public class IssueUpdaterTest { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), "user_uuid"); | |||
issueFieldsSetter.setSeverity(issue, BLOCKER, context); | |||
SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, null, false); | |||
SearchResponseData preloadedSearchResponseData = underTest.saveIssueAndPreloadSearchResponseData(db.getSession(), issue, context, false); | |||
assertThat(preloadedSearchResponseData.getIssues()) | |||
.hasSize(1); |
@@ -36,6 +36,7 @@ import org.sonar.db.issue.IssueChangeDto; | |||
import org.sonar.db.issue.IssueDbTester; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
@@ -47,6 +48,7 @@ import org.sonar.server.issue.IssueUpdater; | |||
import org.sonar.server.issue.TestIssueChangePostProcessor; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
import org.sonar.server.organization.TestDefaultOrganizationProvider; | |||
@@ -94,7 +96,7 @@ public class AddCommentActionTest { | |||
private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); | |||
private WebIssueStorage serverIssueStorage = new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer); | |||
private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); | |||
private IssueUpdater issueUpdater = new IssueUpdater(dbClient, serverIssueStorage, mock(NotificationManager.class), issueChangePostProcessor); | |||
private IssueUpdater issueUpdater = new IssueUpdater(dbClient, serverIssueStorage, mock(NotificationManager.class), issueChangePostProcessor, new IssuesChangesNotificationSerializer()); | |||
private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class); | |||
private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); | |||
@@ -210,7 +212,9 @@ public class AddCommentActionTest { | |||
} | |||
private void loginWithBrowsePermission(IssueDto issueDto, String permission) { | |||
userSession.logIn("john").addProjectPermission(permission, | |||
UserDto user = dbTester.users().insertUser("john"); | |||
userSession.logIn(user) | |||
.addProjectPermission(permission, | |||
dbClient.componentDao().selectByUuid(dbTester.getSession(), issueDto.getProjectUuid()).get(), | |||
dbClient.componentDao().selectByUuid(dbTester.getSession(), issueDto.getComponentUuid()).get()); | |||
} |
@@ -43,6 +43,7 @@ import org.sonar.server.issue.TestIssueChangePostProcessor; | |||
import org.sonar.server.issue.WebIssueStorage; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
import org.sonar.server.organization.TestDefaultOrganizationProvider; | |||
@@ -83,10 +84,11 @@ public class AssignActionTest { | |||
private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); | |||
private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class); | |||
private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); | |||
private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer(); | |||
private AssignAction underTest = new AssignAction(system2, userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), | |||
new IssueUpdater(dbClient, | |||
new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer), | |||
mock(NotificationManager.class), issueChangePostProcessor), | |||
mock(NotificationManager.class), issueChangePostProcessor, issuesChangesSerializer), | |||
responseWriter); | |||
private WsActionTester ws = new WsActionTester(underTest); | |||
@@ -282,8 +284,8 @@ public class AssignActionTest { | |||
} | |||
private void setUserWithPermission(IssueDto issue, String permission) { | |||
insertUser(CURRENT_USER_LOGIN); | |||
userSession.logIn(CURRENT_USER_LOGIN) | |||
UserDto user = insertUser(CURRENT_USER_LOGIN); | |||
userSession.logIn(user) | |||
.addProjectPermission(permission, | |||
dbClient.componentDao().selectByUuid(db.getSession(), issue.getProjectUuid()).get(), | |||
dbClient.componentDao().selectByUuid(db.getSession(), issue.getComponentUuid()).get()); |
@@ -51,7 +51,11 @@ import org.sonar.server.issue.TransitionService; | |||
import org.sonar.server.issue.WebIssueStorage; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.IssueChangeNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotification; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.ChangedIssue; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationBuilder.UserChange; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.issue.workflow.FunctionExecutor; | |||
import org.sonar.server.issue.workflow.IssueWorkflow; | |||
import org.sonar.server.notification.NotificationManager; | |||
@@ -77,6 +81,7 @@ import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.verifyZeroInteractions; | |||
import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; | |||
import static org.sonar.api.issue.Issue.STATUS_CLOSED; | |||
import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; | |||
import static org.sonar.api.issue.Issue.STATUS_OPEN; | |||
import static org.sonar.api.rule.Severity.MAJOR; | |||
import static org.sonar.api.rule.Severity.MINOR; | |||
@@ -87,6 +92,10 @@ import static org.sonar.api.web.UserRole.ISSUE_ADMIN; | |||
import static org.sonar.api.web.UserRole.USER; | |||
import static org.sonar.db.component.ComponentTesting.newFileDto; | |||
import static org.sonar.db.issue.IssueChangeDto.TYPE_COMMENT; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.projectBranchOf; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.projectOf; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.ruleOf; | |||
import static org.sonar.server.issue.notification.IssuesChangesNotificationBuilderTesting.userOf; | |||
public class BulkChangeActionTest { | |||
@@ -112,9 +121,12 @@ public class BulkChangeActionTest { | |||
new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient))); | |||
private NotificationManager notificationManager = mock(NotificationManager.class); | |||
private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); | |||
private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer(); | |||
private ArgumentCaptor<IssuesChangesNotification> issueChangeNotificationCaptor = ArgumentCaptor.forClass(IssuesChangesNotification.class); | |||
private List<Action> actions = new ArrayList<>(); | |||
private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions, issueChangePostProcessor)); | |||
private WsActionTester tester = new WsActionTester(new BulkChangeAction(system2, userSession, dbClient, issueStorage, notificationManager, actions, | |||
issueChangePostProcessor, issuesChangesSerializer)); | |||
@Before | |||
public void setUp() { | |||
@@ -300,22 +312,30 @@ public class BulkChangeActionTest { | |||
.build()); | |||
checkResponse(response, 1, 1, 0, 0); | |||
ArgumentCaptor<IssueChangeNotification> issueChangeNotificationCaptor = ArgumentCaptor.forClass(IssueChangeNotification.class); | |||
verify(notificationManager).scheduleForSending(issueChangeNotificationCaptor.capture()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("key")).isEqualTo(issue.getKey()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("componentName")).isEqualTo(file.longName()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("projectName")).isEqualTo(project.name()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("projectKey")).isEqualTo(project.getDbKey()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("ruleName")).isEqualTo(rule.getName()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("changeAuthor")).isEqualTo(user.getLogin()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("branch")).isNull(); | |||
IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotificationCaptor.getValue()); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changedIssue = builder.getIssues().iterator().next(); | |||
assertThat(changedIssue.getKey()).isEqualTo(issue.getKey()); | |||
assertThat(changedIssue.getProject().getUuid()).isEqualTo(project.uuid()); | |||
assertThat(changedIssue.getProject().getKey()).isEqualTo(project.getKey()); | |||
assertThat(changedIssue.getProject().getProjectName()).isEqualTo(project.name()); | |||
assertThat(changedIssue.getProject().getBranchName()).isEmpty(); | |||
assertThat(changedIssue.getRule().getKey()).isEqualTo(rule.getKey()); | |||
assertThat(changedIssue.getRule().getName()).isEqualTo(rule.getName()); | |||
assertThat(builder.getChange().getDate()).isEqualTo(NOW); | |||
assertThat(builder.getChange()).isInstanceOf(UserChange.class); | |||
UserChange userChange = (UserChange) builder.getChange(); | |||
assertThat(userChange.getUser().getUuid()).isEqualTo(user.getUuid()); | |||
assertThat(userChange.getUser().getLogin()).isEqualTo(user.getLogin()); | |||
assertThat(userChange.getUser().getName()).contains(user.getName()); | |||
} | |||
@Test | |||
public void hotspots_are_ignored_and_no_notification_is_sent() { | |||
UserDto user = db.users().insertUser(); | |||
userSession.logIn(user); | |||
ComponentDto project = db.components().insertPrivateProject(); | |||
ComponentDto project = db.components().insertMainBranch(); | |||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||
addUserProjectPermissions(user, project, USER, ISSUE_ADMIN); | |||
RuleDefinitionDto rule = db.rules().insert(); | |||
@@ -351,22 +371,23 @@ public class BulkChangeActionTest { | |||
.build()); | |||
checkResponse(response, 1, 1, 0, 0); | |||
ArgumentCaptor<IssueChangeNotification> issueChangeNotificationCaptor = ArgumentCaptor.forClass(IssueChangeNotification.class); | |||
verify(notificationManager).scheduleForSending(issueChangeNotificationCaptor.capture()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("key")).isEqualTo(issue.getKey()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("componentName")).isEqualTo(fileOnBranch.longName()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("projectName")).isEqualTo(project.name()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("projectKey")).isEqualTo(project.getDbKey()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("ruleName")).isEqualTo(rule.getName()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("changeAuthor")).isEqualTo(user.getLogin()); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("branch")).isEqualTo("feature"); | |||
IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotificationCaptor.getValue()); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changedIssue = builder.getIssues().iterator().next(); | |||
assertThat(changedIssue.getKey()).isEqualTo(issue.getKey()); | |||
assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_CONFIRMED); | |||
assertThat(changedIssue.getNewResolution()).isEmpty(); | |||
assertThat(changedIssue.getAssignee()).isEmpty(); | |||
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule)); | |||
assertThat(changedIssue.getProject()).isEqualTo(projectBranchOf(db, branch)); | |||
assertThat(builder.getChange()).isEqualTo(new UserChange(NOW, userOf(user))); | |||
verifyPostProcessorCalled(fileOnBranch); | |||
} | |||
@Test | |||
public void send_notification_on_short_branch() { | |||
BranchType branchType = BranchType.SHORT; | |||
verifySendNoNotification(branchType); | |||
public void send_no_notification_on_short_branch() { | |||
verifySendNoNotification(BranchType.SHORT); | |||
} | |||
@Test | |||
@@ -418,11 +439,18 @@ public class BulkChangeActionTest { | |||
.build()); | |||
checkResponse(response, 3, 1, 2, 0); | |||
ArgumentCaptor<IssueChangeNotification> issueChangeNotificationCaptor = ArgumentCaptor.forClass(IssueChangeNotification.class); | |||
verify(notificationManager).scheduleForSending(issueChangeNotificationCaptor.capture()); | |||
assertThat(issueChangeNotificationCaptor.getAllValues()).hasSize(1); | |||
assertThat(issueChangeNotificationCaptor.getValue().getFieldValue("key")).isEqualTo(issue3.getKey()); | |||
verifyPostProcessorCalled(file); | |||
IssuesChangesNotificationBuilder builder = issuesChangesSerializer.from(issueChangeNotificationCaptor.getValue()); | |||
assertThat(builder.getIssues()).hasSize(1); | |||
ChangedIssue changedIssue = builder.getIssues().iterator().next(); | |||
assertThat(changedIssue.getKey()).isEqualTo(issue3.getKey()); | |||
assertThat(changedIssue.getNewStatus()).isEqualTo(STATUS_OPEN); | |||
assertThat(changedIssue.getNewResolution()).isEmpty(); | |||
assertThat(changedIssue.getAssignee()).isEmpty(); | |||
assertThat(changedIssue.getRule()).isEqualTo(ruleOf(rule)); | |||
assertThat(changedIssue.getProject()).isEqualTo(projectOf(project)); | |||
assertThat(builder.getChange()).isEqualTo(new UserChange(NOW, userOf(user))); | |||
} | |||
@Test |
@@ -46,6 +46,7 @@ import org.sonar.server.issue.TransitionService; | |||
import org.sonar.server.issue.WebIssueStorage; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.issue.workflow.FunctionExecutor; | |||
import org.sonar.server.issue.workflow.IssueWorkflow; | |||
import org.sonar.server.notification.NotificationManager; | |||
@@ -96,9 +97,10 @@ public class DoTransitionActionTest { | |||
private OperationResponseWriter responseWriter = mock(OperationResponseWriter.class); | |||
private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); | |||
private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); | |||
private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer(); | |||
private IssueUpdater issueUpdater = new IssueUpdater(dbClient, | |||
new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer), mock(NotificationManager.class), | |||
issueChangePostProcessor); | |||
issueChangePostProcessor, issuesChangesSerializer); | |||
private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); | |||
private WsAction underTest = new DoTransitionAction(dbClient, userSession, new IssueFinder(dbClient, userSession), issueUpdater, transitionService, responseWriter, system2); | |||
@@ -115,7 +117,7 @@ public class DoTransitionActionTest { | |||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||
RuleDefinitionDto rule = db.rules().insert(); | |||
IssueDto issue = db.issues().insert(rule, project, file, i -> i.setStatus(STATUS_OPEN).setResolution(null)); | |||
userSession.logIn().addProjectPermission(USER, project, file); | |||
userSession.logIn(db.users().insertUser()).addProjectPermission(USER, project, file); | |||
call(issue.getKey(), "confirm"); | |||
@@ -37,6 +37,7 @@ import org.sonar.db.issue.IssueDbTester; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
@@ -47,6 +48,7 @@ import org.sonar.server.issue.IssueUpdater; | |||
import org.sonar.server.issue.TestIssueChangePostProcessor; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
import org.sonar.server.organization.TestDefaultOrganizationProvider; | |||
@@ -91,9 +93,10 @@ public class SetSeverityActionTest { | |||
private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); | |||
private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); | |||
private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer(); | |||
private WsActionTester tester = new WsActionTester(new SetSeverityAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), | |||
new IssueUpdater(dbClient, | |||
new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer), mock(NotificationManager.class), issueChangePostProcessor), | |||
new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer), mock(NotificationManager.class), issueChangePostProcessor, issuesChangesSerializer), | |||
responseWriter)); | |||
@Test | |||
@@ -187,12 +190,15 @@ public class SetSeverityActionTest { | |||
} | |||
private void logInAndAddProjectPermission(IssueDto issueDto, String permission) { | |||
userSession.logIn("john").addProjectPermission(permission, dbClient.componentDao().selectByUuid(dbTester.getSession(), issueDto.getProjectUuid()).get()); | |||
UserDto user = dbTester.users().insertUser("john"); | |||
userSession.logIn(user) | |||
.addProjectPermission(permission, dbClient.componentDao().selectByUuid(dbTester.getSession(), issueDto.getProjectUuid()).get()); | |||
} | |||
private void setUserWithBrowseAndAdministerIssuePermission(IssueDto issueDto) { | |||
ComponentDto project = dbClient.componentDao().selectByUuid(dbTester.getSession(), issueDto.getProjectUuid()).get(); | |||
userSession.logIn("john") | |||
UserDto user = dbTester.users().insertUser("john"); | |||
userSession.logIn(user) | |||
.addProjectPermission(ISSUE_ADMIN, project) | |||
.addProjectPermission(USER, project); | |||
} |
@@ -39,6 +39,7 @@ import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.db.issue.IssueTesting; | |||
import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.db.user.UserDto; | |||
import org.sonar.server.es.EsTester; | |||
import org.sonar.server.exceptions.ForbiddenException; | |||
import org.sonar.server.exceptions.UnauthorizedException; | |||
@@ -49,6 +50,7 @@ import org.sonar.server.issue.TestIssueChangePostProcessor; | |||
import org.sonar.server.issue.WebIssueStorage; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
import org.sonar.server.organization.TestDefaultOrganizationProvider; | |||
@@ -88,10 +90,12 @@ public class SetTagsActionTest { | |||
private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); | |||
private ArgumentCaptor<SearchResponseData> preloadedSearchResponseDataCaptor = ArgumentCaptor.forClass(SearchResponseData.class); | |||
private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); | |||
private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer(); | |||
private WsActionTester ws = new WsActionTester(new SetTagsAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), | |||
new IssueUpdater(dbClient, | |||
new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer), mock(NotificationManager.class), issueChangePostProcessor), | |||
new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer), mock(NotificationManager.class), | |||
issueChangePostProcessor, issuesChangesSerializer), | |||
responseWriter)); | |||
@Test | |||
@@ -243,13 +247,17 @@ public class SetTagsActionTest { | |||
} | |||
private void logIn(IssueDto issueDto) { | |||
userSession.logIn("john").registerComponents( | |||
dbClient.componentDao().selectByUuid(db.getSession(), issueDto.getProjectUuid()).get(), | |||
dbClient.componentDao().selectByUuid(db.getSession(), issueDto.getComponentUuid()).get()); | |||
UserDto user = db.users().insertUser("john"); | |||
userSession.logIn(user) | |||
.registerComponents( | |||
dbClient.componentDao().selectByUuid(db.getSession(), issueDto.getProjectUuid()).get(), | |||
dbClient.componentDao().selectByUuid(db.getSession(), issueDto.getComponentUuid()).get()); | |||
} | |||
private void logInAndAddProjectPermission(IssueDto issueDto, String permission) { | |||
userSession.logIn("john").addProjectPermission(permission, dbClient.componentDao().selectByUuid(db.getSession(), issueDto.getProjectUuid()).get()); | |||
UserDto user = db.users().insertUser("john"); | |||
userSession.logIn(user) | |||
.addProjectPermission(permission, dbClient.componentDao().selectByUuid(db.getSession(), issueDto.getProjectUuid()).get()); | |||
} | |||
private void verifyContentOfPreloadedSearchResponseData(IssueDto issue) { |
@@ -47,6 +47,7 @@ import org.sonar.server.issue.IssueUpdater; | |||
import org.sonar.server.issue.TestIssueChangePostProcessor; | |||
import org.sonar.server.issue.index.IssueIndexer; | |||
import org.sonar.server.issue.index.IssueIteratorFactory; | |||
import org.sonar.server.issue.notification.IssuesChangesNotificationSerializer; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.organization.DefaultOrganizationProvider; | |||
import org.sonar.server.organization.TestDefaultOrganizationProvider; | |||
@@ -94,10 +95,11 @@ public class SetTypeActionTest { | |||
private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); | |||
private TestIssueChangePostProcessor issueChangePostProcessor = new TestIssueChangePostProcessor(); | |||
private IssuesChangesNotificationSerializer issuesChangesSerializer = new IssuesChangesNotificationSerializer(); | |||
private WsActionTester tester = new WsActionTester(new SetTypeAction(userSession, dbClient, new IssueFinder(dbClient, userSession), new IssueFieldsSetter(), | |||
new IssueUpdater(dbClient, | |||
new WebIssueStorage(system2, dbClient, new DefaultRuleFinder(dbClient, defaultOrganizationProvider), issueIndexer), mock(NotificationManager.class), | |||
issueChangePostProcessor), | |||
issueChangePostProcessor, issuesChangesSerializer), | |||
responseWriter, system2)); | |||
@Test | |||
@@ -207,7 +209,7 @@ public class SetTypeActionTest { | |||
private void setUserWithBrowseAndAdministerIssuePermission(IssueDto issueDto) { | |||
ComponentDto project = dbClient.componentDao().selectByUuid(dbTester.getSession(), issueDto.getProjectUuid()).get(); | |||
userSession.logIn("john") | |||
userSession.logIn(dbTester.users().insertUser("john")) | |||
.addProjectPermission(ISSUE_ADMIN, project) | |||
.addProjectPermission(USER, project); | |||
} |
@@ -139,7 +139,7 @@ public class EmailNotificationChannelTest { | |||
EmailMessage emailMessage = new EmailMessage() | |||
.setTo("user@nowhere") | |||
.setSubject("Foo") | |||
.setMessage("Bar"); | |||
.setPlainTextMessage("Bar"); | |||
boolean delivered = underTest.deliver(emailMessage); | |||
assertThat(smtpServer.getMessages()).isEmpty(); | |||
assertThat(delivered).isFalse(); | |||
@@ -153,7 +153,7 @@ public class EmailNotificationChannelTest { | |||
.setFrom("Full Username") | |||
.setTo("user@nowhere") | |||
.setSubject("Review #3") | |||
.setMessage("I'll take care of this violation."); | |||
.setPlainTextMessage("I'll take care of this violation."); | |||
boolean delivered = underTest.deliver(emailMessage); | |||
List<WiserMessage> messages = smtpServer.getMessages(); | |||
@@ -182,7 +182,7 @@ public class EmailNotificationChannelTest { | |||
EmailMessage emailMessage = new EmailMessage() | |||
.setTo("user@nowhere") | |||
.setSubject("Foo") | |||
.setMessage("Bar"); | |||
.setPlainTextMessage("Bar"); | |||
boolean delivered = underTest.deliver(emailMessage); | |||
List<WiserMessage> messages = smtpServer.getMessages(); | |||
@@ -213,7 +213,7 @@ public class EmailNotificationChannelTest { | |||
EmailMessage emailMessage = new EmailMessage() | |||
.setTo("user@nowhere") | |||
.setSubject("Foo") | |||
.setMessage("Bar"); | |||
.setPlainTextMessage("Bar"); | |||
boolean delivered = underTest.deliver(emailMessage); | |||
assertThat(delivered).isFalse(); | |||
@@ -291,8 +291,8 @@ public class EmailNotificationChannelTest { | |||
Notification notification3 = mock(Notification.class); | |||
EmailTemplate template1 = mock(EmailTemplate.class); | |||
EmailTemplate template3 = mock(EmailTemplate.class); | |||
EmailMessage emailMessage1 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setMessage("msg11"); | |||
EmailMessage emailMessage3 = new EmailMessage().setTo(recipientEmail).setSubject("sub3").setMessage("msg3"); | |||
EmailMessage emailMessage1 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11"); | |||
EmailMessage emailMessage3 = new EmailMessage().setTo(recipientEmail).setSubject("sub3").setPlainTextMessage("msg3"); | |||
when(template1.format(notification1)).thenReturn(emailMessage1); | |||
when(template3.format(notification3)).thenReturn(emailMessage3); | |||
Set<EmailDeliveryRequest> requests = Stream.of(notification1, notification2, notification3) | |||
@@ -333,8 +333,8 @@ public class EmailNotificationChannelTest { | |||
Notification notification1 = mock(Notification.class); | |||
EmailTemplate template11 = mock(EmailTemplate.class); | |||
EmailTemplate template12 = mock(EmailTemplate.class); | |||
EmailMessage emailMessage11 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setMessage("msg11"); | |||
EmailMessage emailMessage12 = new EmailMessage().setTo(recipientEmail).setSubject("sub12").setMessage("msg12"); | |||
EmailMessage emailMessage11 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11"); | |||
EmailMessage emailMessage12 = new EmailMessage().setTo(recipientEmail).setSubject("sub12").setPlainTextMessage("msg12"); | |||
when(template11.format(notification1)).thenReturn(emailMessage11); | |||
when(template12.format(notification1)).thenReturn(emailMessage12); | |||
EmailDeliveryRequest request = new EmailDeliveryRequest(recipientEmail, notification1); |
@@ -22,7 +22,7 @@ package org.sonar.server.notification.ws; | |||
import org.junit.Test; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import org.sonar.api.notifications.NotificationChannel; | |||
import org.sonar.server.issue.notification.DoNotFixNotificationHandler; | |||
import org.sonar.server.issue.notification.FPOrWontFixNotificationHandler; | |||
import org.sonar.server.issue.notification.MyNewIssuesNotificationHandler; | |||
import org.sonar.server.issue.notification.NewIssuesNotificationHandler; | |||
import org.sonar.server.notification.NotificationCenter; | |||
@@ -45,7 +45,7 @@ public class DispatchersImplTest { | |||
NotificationDispatcherMetadata.create(QGChangeNotificationHandler.KEY) | |||
.setProperty(GLOBAL_NOTIFICATION, "true") | |||
.setProperty(PER_PROJECT_NOTIFICATION, "true"), | |||
NotificationDispatcherMetadata.create(DoNotFixNotificationHandler.KEY) | |||
NotificationDispatcherMetadata.create(FPOrWontFixNotificationHandler.KEY) | |||
.setProperty(GLOBAL_NOTIFICATION, "false") | |||
.setProperty(PER_PROJECT_NOTIFICATION, "true") | |||
}, | |||
@@ -77,7 +77,7 @@ public class DispatchersImplTest { | |||
underTest.start(); | |||
assertThat(underTest.getProjectDispatchers()).containsExactly( | |||
QGChangeNotificationHandler.KEY, DoNotFixNotificationHandler.KEY, MyNewIssuesNotificationHandler.KEY); | |||
QGChangeNotificationHandler.KEY, FPOrWontFixNotificationHandler.KEY, MyNewIssuesNotificationHandler.KEY); | |||
} | |||
@Test | |||
@@ -87,6 +87,6 @@ public class DispatchersImplTest { | |||
underTest.start(); | |||
assertThat(underTest.getProjectDispatchers()).containsOnly( | |||
MyNewIssuesNotificationHandler.KEY, QGChangeNotificationHandler.KEY, DoNotFixNotificationHandler.KEY); | |||
MyNewIssuesNotificationHandler.KEY, QGChangeNotificationHandler.KEY, FPOrWontFixNotificationHandler.KEY); | |||
} | |||
} |
@@ -375,5 +375,4 @@ public class UserSessionRule implements TestRule, UserSession { | |||
ensureAbstractMockUserSession().addOrganizationMembership(organization); | |||
return this; | |||
} | |||
} |
@@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableList; | |||
import com.google.common.collect.ImmutableListMultimap; | |||
import com.google.common.collect.ImmutableMap; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.google.common.collect.ImmutableSetMultimap; | |||
import java.util.ArrayList; | |||
import java.util.EnumSet; | |||
import java.util.HashMap; | |||
@@ -37,12 +38,15 @@ import java.util.function.Function; | |||
import java.util.function.Supplier; | |||
import java.util.stream.Collector; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.Stream; | |||
import static java.util.Objects.requireNonNull; | |||
public final class MoreCollectors { | |||
private static final int DEFAULT_HASHMAP_CAPACITY = 0; | |||
private static final String KEY_FUNCTION_CANT_RETURN_NULL_MESSAGE = "Key function can't return null"; | |||
private static final String VALUE_FUNCTION_CANT_RETURN_NULL_MESSAGE = "Value function can't return null"; | |||
private MoreCollectors() { | |||
// prevents instantiation | |||
@@ -247,11 +251,11 @@ public final class MoreCollectors { | |||
*/ | |||
public static <K, E, V> Collector<E, Map<K, V>, ImmutableMap<K, V>> uniqueIndex(Function<? super E, K> keyFunction, | |||
Function<? super E, V> valueFunction, int expectedSize) { | |||
requireNonNull(keyFunction, "Key function can't be null"); | |||
requireNonNull(valueFunction, "Value function can't be null"); | |||
verifyKeyAndValueFunctions(keyFunction, valueFunction); | |||
BiConsumer<Map<K, V>, E> accumulator = (map, element) -> { | |||
K key = requireNonNull(keyFunction.apply(element), "Key function can't return null"); | |||
V value = requireNonNull(valueFunction.apply(element), "Value function can't return null"); | |||
K key = requireNonNull(keyFunction.apply(element), KEY_FUNCTION_CANT_RETURN_NULL_MESSAGE); | |||
V value = requireNonNull(valueFunction.apply(element), VALUE_FUNCTION_CANT_RETURN_NULL_MESSAGE); | |||
putAndFailOnDuplicateKey(map, key, value); | |||
}; | |||
@@ -328,11 +332,11 @@ public final class MoreCollectors { | |||
*/ | |||
public static <K, E, V> Collector<E, ImmutableListMultimap.Builder<K, V>, ImmutableListMultimap<K, V>> index(Function<? super E, K> keyFunction, | |||
Function<? super E, V> valueFunction) { | |||
requireNonNull(keyFunction, "Key function can't be null"); | |||
requireNonNull(valueFunction, "Value function can't be null"); | |||
verifyKeyAndValueFunctions(keyFunction, valueFunction); | |||
BiConsumer<ImmutableListMultimap.Builder<K, V>, E> accumulator = (map, element) -> { | |||
K key = requireNonNull(keyFunction.apply(element), "Key function can't return null"); | |||
V value = requireNonNull(valueFunction.apply(element), "Value function can't return null"); | |||
K key = requireNonNull(keyFunction.apply(element), KEY_FUNCTION_CANT_RETURN_NULL_MESSAGE); | |||
V value = requireNonNull(valueFunction.apply(element), VALUE_FUNCTION_CANT_RETURN_NULL_MESSAGE); | |||
map.put(key, value); | |||
}; | |||
@@ -349,6 +353,93 @@ public final class MoreCollectors { | |||
ImmutableListMultimap.Builder::build); | |||
} | |||
/** | |||
* Creates an {@link com.google.common.collect.ImmutableSetMultimap} from the stream where the values are the values | |||
* in the stream and the keys are the result of the provided {@link Function keyFunction} applied to each value in the | |||
* stream. | |||
* | |||
* <p> | |||
* Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a | |||
* {@link NullPointerException} will be thrown. | |||
* </p> | |||
* | |||
* @throws NullPointerException if {@code keyFunction} or {@code valueFunction} is {@code null}. | |||
* @throws NullPointerException if result of {@code keyFunction} or {@code valueFunction} is {@code null}. | |||
*/ | |||
public static <K, E> Collector<E, ImmutableSetMultimap.Builder<K, E>, ImmutableSetMultimap<K, E>> unorderedIndex(Function<? super E, K> keyFunction) { | |||
return unorderedIndex(keyFunction, Function.identity()); | |||
} | |||
/** | |||
* Creates an {@link com.google.common.collect.ImmutableSetMultimap} from the stream where the values are the result | |||
* of {@link Function valueFunction} applied to the values in the stream and the keys are the result of the provided | |||
* {@link Function keyFunction} applied to each value in the stream. | |||
* | |||
* <p> | |||
* Neither {@link Function keyFunction} nor {@link Function valueFunction} can return {@code null}, otherwise a | |||
* {@link NullPointerException} will be thrown. | |||
* </p> | |||
* | |||
* @throws NullPointerException if {@code keyFunction} or {@code valueFunction} is {@code null}. | |||
* @throws NullPointerException if result of {@code keyFunction} or {@code valueFunction} is {@code null}. | |||
*/ | |||
public static <K, E, V> Collector<E, ImmutableSetMultimap.Builder<K, V>, ImmutableSetMultimap<K, V>> unorderedIndex(Function<? super E, K> keyFunction, | |||
Function<? super E, V> valueFunction) { | |||
verifyKeyAndValueFunctions(keyFunction, valueFunction); | |||
BiConsumer<ImmutableSetMultimap.Builder<K, V>, E> accumulator = (map, element) -> { | |||
K key = requireNonNull(keyFunction.apply(element), KEY_FUNCTION_CANT_RETURN_NULL_MESSAGE); | |||
V value = requireNonNull(valueFunction.apply(element), VALUE_FUNCTION_CANT_RETURN_NULL_MESSAGE); | |||
map.put(key, value); | |||
}; | |||
BinaryOperator<ImmutableSetMultimap.Builder<K, V>> merger = (m1, m2) -> { | |||
for (Map.Entry<K, V> entry : m2.build().entries()) { | |||
m1.put(entry.getKey(), entry.getValue()); | |||
} | |||
return m1; | |||
}; | |||
return Collector.of( | |||
ImmutableSetMultimap::builder, | |||
accumulator, | |||
merger, | |||
ImmutableSetMultimap.Builder::build); | |||
} | |||
/** | |||
* A Collector similar to {@link #unorderedIndex(Function, Function)} except that it expects the {@code valueFunction} | |||
* to return a {@link Stream} which content will be flatten into the returned {@link ImmutableSetMultimap}. | |||
* | |||
* @see #unorderedIndex(Function, Function) | |||
*/ | |||
public static <K, E, V> Collector<E, ImmutableSetMultimap.Builder<K, V>, ImmutableSetMultimap<K, V>> unorderedFlattenIndex( | |||
Function<? super E, K> keyFunction, Function<? super E, Stream<V>> valueFunction) { | |||
verifyKeyAndValueFunctions(keyFunction, valueFunction); | |||
BiConsumer<ImmutableSetMultimap.Builder<K, V>, E> accumulator = (map, element) -> { | |||
K key = requireNonNull(keyFunction.apply(element), KEY_FUNCTION_CANT_RETURN_NULL_MESSAGE); | |||
Stream<V> valueStream = requireNonNull(valueFunction.apply(element), VALUE_FUNCTION_CANT_RETURN_NULL_MESSAGE); | |||
valueStream.forEach(value -> map.put(key, value)); | |||
}; | |||
BinaryOperator<ImmutableSetMultimap.Builder<K, V>> merger = (m1, m2) -> { | |||
for (Map.Entry<K, V> entry : m2.build().entries()) { | |||
m1.put(entry.getKey(), entry.getValue()); | |||
} | |||
return m1; | |||
}; | |||
return Collector.of( | |||
ImmutableSetMultimap::builder, | |||
accumulator, | |||
merger, | |||
ImmutableSetMultimap.Builder::build); | |||
} | |||
private static void verifyKeyAndValueFunctions(Function<?, ?> keyFunction, Function<?, ?> valueFunction) { | |||
requireNonNull(keyFunction, "Key function can't be null"); | |||
requireNonNull(valueFunction, "Value function can't be null"); | |||
} | |||
/** | |||
* Applies the specified {@link Joiner} to the current stream. | |||
* |
@@ -22,7 +22,8 @@ package org.sonar.core.util.stream; | |||
import com.google.common.base.Joiner; | |||
import com.google.common.collect.ImmutableList; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.google.common.collect.Multimap; | |||
import com.google.common.collect.ListMultimap; | |||
import com.google.common.collect.SetMultimap; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
@@ -32,6 +33,7 @@ import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.IntStream; | |||
import java.util.stream.Stream; | |||
import org.junit.Rule; | |||
@@ -48,6 +50,8 @@ import static org.sonar.core.util.stream.MoreCollectors.toHashSet; | |||
import static org.sonar.core.util.stream.MoreCollectors.toList; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
import static org.sonar.core.util.stream.MoreCollectors.unorderedFlattenIndex; | |||
import static org.sonar.core.util.stream.MoreCollectors.unorderedIndex; | |||
public class MoreCollectorsTest { | |||
@@ -57,9 +61,16 @@ public class MoreCollectorsTest { | |||
private static final MyObj MY_OBJ_1_C = new MyObj(1, "C"); | |||
private static final MyObj MY_OBJ_2_B = new MyObj(2, "B"); | |||
private static final MyObj MY_OBJ_3_C = new MyObj(3, "C"); | |||
private static final MyObj2 MY_OBJ2_1_A_X = new MyObj2(1, "A", "X"); | |||
private static final MyObj2 MY_OBJ2_1_C = new MyObj2(1, "C"); | |||
private static final MyObj2 MY_OBJ2_2_B = new MyObj2(2, "B"); | |||
private static final MyObj2 MY_OBJ2_3_C = new MyObj2(3, "C"); | |||
private static final List<MyObj> SINGLE_ELEMENT_LIST = Arrays.asList(MY_OBJ_1_A); | |||
private static final List<MyObj2> SINGLE_ELEMENT2_LIST = Arrays.asList(MY_OBJ2_1_A_X); | |||
private static final List<MyObj> LIST_WITH_DUPLICATE_ID = Arrays.asList(MY_OBJ_1_A, MY_OBJ_2_B, MY_OBJ_1_C); | |||
private static final List<MyObj2> LIST2_WITH_DUPLICATE_ID = Arrays.asList(MY_OBJ2_1_A_X, MY_OBJ2_2_B, MY_OBJ2_1_C); | |||
private static final List<MyObj> LIST = Arrays.asList(MY_OBJ_1_A, MY_OBJ_2_B, MY_OBJ_3_C); | |||
private static final List<MyObj2> LIST2 = Arrays.asList(MY_OBJ2_1_A_X, MY_OBJ2_2_B, MY_OBJ2_3_C); | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@@ -356,6 +367,15 @@ public class MoreCollectorsTest { | |||
assertThat(map.values()).containsExactlyElementsOf(HUGE_SET); | |||
} | |||
@Test | |||
public void uniqueIndex_supports_duplicate_keys() { | |||
ListMultimap<Integer, String> multimap = LIST_WITH_DUPLICATE_ID.stream().collect(index(MyObj::getId, MyObj::getText)); | |||
assertThat(multimap.keySet()).containsOnly(1, 2); | |||
assertThat(multimap.get(1)).containsOnly("A", "C"); | |||
assertThat(multimap.get(2)).containsOnly("B"); | |||
} | |||
@Test | |||
public void index_empty_stream_returns_empty_map() { | |||
assertThat(Collections.<MyObj>emptyList().stream().collect(index(MyObj::getId)).size()).isEqualTo(0); | |||
@@ -409,7 +429,7 @@ public class MoreCollectorsTest { | |||
@Test | |||
public void index_supports_duplicate_keys() { | |||
Multimap<Integer, MyObj> multimap = LIST_WITH_DUPLICATE_ID.stream().collect(index(MyObj::getId)); | |||
ListMultimap<Integer, MyObj> multimap = LIST_WITH_DUPLICATE_ID.stream().collect(index(MyObj::getId)); | |||
assertThat(multimap.keySet()).containsOnly(1, 2); | |||
assertThat(multimap.get(1)).containsOnly(MY_OBJ_1_A, MY_OBJ_1_C); | |||
@@ -417,17 +437,104 @@ public class MoreCollectorsTest { | |||
} | |||
@Test | |||
public void uniqueIndex_supports_duplicate_keys() { | |||
Multimap<Integer, String> multimap = LIST_WITH_DUPLICATE_ID.stream().collect(index(MyObj::getId, MyObj::getText)); | |||
public void index_returns_ListMultimap() { | |||
ListMultimap<Integer, MyObj> multimap = LIST.stream().collect(index(MyObj::getId)); | |||
assertThat(multimap.size()).isEqualTo(3); | |||
Map<Integer, Collection<MyObj>> map = multimap.asMap(); | |||
assertThat(map.get(1)).containsOnly(MY_OBJ_1_A); | |||
assertThat(map.get(2)).containsOnly(MY_OBJ_2_B); | |||
assertThat(map.get(3)).containsOnly(MY_OBJ_3_C); | |||
} | |||
@Test | |||
public void index_with_valueFunction_returns_ListMultimap() { | |||
ListMultimap<Integer, String> multimap = LIST.stream().collect(index(MyObj::getId, MyObj::getText)); | |||
assertThat(multimap.size()).isEqualTo(3); | |||
Map<Integer, Collection<String>> map = multimap.asMap(); | |||
assertThat(map.get(1)).containsOnly("A"); | |||
assertThat(map.get(2)).containsOnly("B"); | |||
assertThat(map.get(3)).containsOnly("C"); | |||
} | |||
@Test | |||
public void index_parallel_stream() { | |||
ListMultimap<String, String> multimap = HUGE_LIST.parallelStream().collect(index(identity())); | |||
assertThat(multimap.keySet()).isEqualTo(HUGE_SET); | |||
} | |||
@Test | |||
public void index_with_valueFunction_parallel_stream() { | |||
ListMultimap<String, String> multimap = HUGE_LIST.parallelStream().collect(index(identity(), identity())); | |||
assertThat(multimap.keySet()).isEqualTo(HUGE_SET); | |||
} | |||
@Test | |||
public void unorderedIndex_empty_stream_returns_empty_map() { | |||
assertThat(Collections.<MyObj>emptyList().stream().collect(unorderedIndex(MyObj::getId)).size()).isEqualTo(0); | |||
assertThat(Collections.<MyObj>emptyList().stream().collect(unorderedIndex(MyObj::getId, MyObj::getText)).size()).isEqualTo(0); | |||
} | |||
@Test | |||
public void unorderedIndex_fails_if_key_function_is_null() { | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("Key function can't be null"); | |||
unorderedIndex(null); | |||
} | |||
@Test | |||
public void unorderedIndex_with_valueFunction_fails_if_key_function_is_null() { | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("Key function can't be null"); | |||
unorderedIndex(null, MyObj::getText); | |||
} | |||
@Test | |||
public void unorderedIndex_with_valueFunction_fails_if_value_function_is_null() { | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("Value function can't be null"); | |||
unorderedIndex(MyObj::getId, null); | |||
} | |||
@Test | |||
public void unorderedIndex_fails_if_key_function_returns_null() { | |||
expectKeyFunctionCantReturnNullNPE(); | |||
SINGLE_ELEMENT_LIST.stream().collect(unorderedIndex(s -> null)); | |||
} | |||
@Test | |||
public void unorderedIndex_with_valueFunction_fails_if_key_function_returns_null() { | |||
expectKeyFunctionCantReturnNullNPE(); | |||
SINGLE_ELEMENT_LIST.stream().collect(unorderedIndex(s -> null, MyObj::getText)); | |||
} | |||
@Test | |||
public void unorderedIndex_with_valueFunction_fails_if_value_function_returns_null() { | |||
expectValueFunctionCantReturnNullNPE(); | |||
SINGLE_ELEMENT_LIST.stream().collect(unorderedIndex(MyObj::getId, s -> null)); | |||
} | |||
@Test | |||
public void unorderedIndex_supports_duplicate_keys() { | |||
SetMultimap<Integer, MyObj> multimap = LIST_WITH_DUPLICATE_ID.stream().collect(unorderedIndex(MyObj::getId)); | |||
assertThat(multimap.keySet()).containsOnly(1, 2); | |||
assertThat(multimap.get(1)).containsOnly("A", "C"); | |||
assertThat(multimap.get(2)).containsOnly("B"); | |||
assertThat(multimap.get(1)).containsOnly(MY_OBJ_1_A, MY_OBJ_1_C); | |||
assertThat(multimap.get(2)).containsOnly(MY_OBJ_2_B); | |||
} | |||
@Test | |||
public void index_returns_multimap() { | |||
Multimap<Integer, MyObj> multimap = LIST.stream().collect(index(MyObj::getId)); | |||
public void unorderedIndex_returns_SetMultimap() { | |||
SetMultimap<Integer, MyObj> multimap = LIST.stream().collect(unorderedIndex(MyObj::getId)); | |||
assertThat(multimap.size()).isEqualTo(3); | |||
Map<Integer, Collection<MyObj>> map = multimap.asMap(); | |||
@@ -437,8 +544,8 @@ public class MoreCollectorsTest { | |||
} | |||
@Test | |||
public void index_with_valueFunction_returns_multimap() { | |||
Multimap<Integer, String> multimap = LIST.stream().collect(index(MyObj::getId, MyObj::getText)); | |||
public void unorderedIndex_with_valueFunction_returns_SetMultimap() { | |||
SetMultimap<Integer, String> multimap = LIST.stream().collect(unorderedIndex(MyObj::getId, MyObj::getText)); | |||
assertThat(multimap.size()).isEqualTo(3); | |||
Map<Integer, Collection<String>> map = multimap.asMap(); | |||
@@ -448,19 +555,113 @@ public class MoreCollectorsTest { | |||
} | |||
@Test | |||
public void index_parallel_stream() { | |||
Multimap<String, String> multimap = HUGE_LIST.parallelStream().collect(index(identity())); | |||
public void unorderedIndex_parallel_stream() { | |||
SetMultimap<String, String> multimap = HUGE_LIST.parallelStream().collect(unorderedIndex(identity())); | |||
assertThat(multimap.keySet()).isEqualTo(HUGE_SET); | |||
} | |||
@Test | |||
public void index_with_valueFunction_parallel_stream() { | |||
Multimap<String, String> multimap = HUGE_LIST.parallelStream().collect(index(identity(), identity())); | |||
public void unorderedIndex_with_valueFunction_parallel_stream() { | |||
SetMultimap<String, String> multimap = HUGE_LIST.parallelStream().collect(unorderedIndex(identity(), identity())); | |||
assertThat(multimap.keySet()).isEqualTo(HUGE_SET); | |||
} | |||
@Test | |||
public void unorderedFlattenIndex_empty_stream_returns_empty_map() { | |||
assertThat(Collections.<MyObj2>emptyList().stream() | |||
.collect(unorderedFlattenIndex(MyObj2::getId, MyObj2::getTexts)) | |||
.size()).isEqualTo(0); | |||
} | |||
@Test | |||
public void unorderedFlattenIndex_with_valueFunction_fails_if_key_function_is_null() { | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("Key function can't be null"); | |||
unorderedFlattenIndex(null, MyObj2::getTexts); | |||
} | |||
@Test | |||
public void unorderedFlattenIndex_with_valueFunction_fails_if_value_function_is_null() { | |||
expectedException.expect(NullPointerException.class); | |||
expectedException.expectMessage("Value function can't be null"); | |||
unorderedFlattenIndex(MyObj2::getId, null); | |||
} | |||
@Test | |||
public void unorderedFlattenIndex_with_valueFunction_fails_if_key_function_returns_null() { | |||
expectKeyFunctionCantReturnNullNPE(); | |||
SINGLE_ELEMENT2_LIST.stream().collect(unorderedFlattenIndex(s -> null, MyObj2::getTexts)); | |||
} | |||
@Test | |||
public void unorderedFlattenIndex_with_valueFunction_fails_if_value_function_returns_null() { | |||
expectValueFunctionCantReturnNullNPE(); | |||
SINGLE_ELEMENT2_LIST.stream().collect(unorderedFlattenIndex(MyObj2::getId, s -> null)); | |||
} | |||
@Test | |||
public void unorderedFlattenIndex_supports_duplicate_keys() { | |||
SetMultimap<Integer, String> multimap = LIST2_WITH_DUPLICATE_ID.stream() | |||
.collect(unorderedFlattenIndex(MyObj2::getId, MyObj2::getTexts)); | |||
assertThat(multimap.keySet()).containsOnly(1, 2); | |||
assertThat(multimap.get(1)).containsOnly("A", "X", "C"); | |||
assertThat(multimap.get(2)).containsOnly("B"); | |||
} | |||
@Test | |||
public void unorderedFlattenIndex_with_valueFunction_returns_SetMultimap() { | |||
SetMultimap<Integer, String> multimap = LIST2.stream() | |||
.collect(unorderedFlattenIndex(MyObj2::getId, MyObj2::getTexts)); | |||
assertThat(multimap.size()).isEqualTo(4); | |||
Map<Integer, Collection<String>> map = multimap.asMap(); | |||
assertThat(map.get(1)).containsOnly("A", "X"); | |||
assertThat(map.get(2)).containsOnly("B"); | |||
assertThat(map.get(3)).containsOnly("C"); | |||
} | |||
@Test | |||
public void unorderedFlattenIndex_with_valueFunction_parallel_stream() { | |||
SetMultimap<String, String> multimap = HUGE_LIST.parallelStream().collect(unorderedFlattenIndex(identity(), Stream::of)); | |||
assertThat(multimap.keySet()).isEqualTo(HUGE_SET); | |||
} | |||
@Test | |||
public void join_on_empty_stream_returns_empty_string() { | |||
assertThat(Collections.emptyList().stream().collect(join(Joiner.on(",")))).isEmpty(); | |||
@@ -532,6 +733,24 @@ public class MoreCollectorsTest { | |||
} | |||
} | |||
private static final class MyObj2 { | |||
private final int id; | |||
private final List<String> texts; | |||
public MyObj2(int id, String... texts) { | |||
this.id = id; | |||
this.texts = Arrays.stream(texts).collect(Collectors.toList()); | |||
} | |||
public int getId() { | |||
return id; | |||
} | |||
public Stream<String> getTexts() { | |||
return texts.stream(); | |||
} | |||
} | |||
private enum MyEnum { | |||
ONE, TWO, THREE | |||
} |
@@ -100,6 +100,10 @@ public class EmailSettings { | |||
.orElse(SERVER_BASE_URL_DEFAULT_VALUE); | |||
} | |||
public String getInstanceName() { | |||
return config.getBoolean("sonar.sonarcloud.enabled").orElse(false) ? "SonarCloud" : "SonarQube"; | |||
} | |||
private String get(String key, String defaultValue) { | |||
return config.get(key).orElse(defaultValue); | |||
} |
@@ -60,6 +60,18 @@ public class EmailSettingsTest { | |||
assertThat(underTest.getServerBaseURL()).isEqualTo("http://www.acme.com"); | |||
} | |||
@Test | |||
public void getInstanceName_returns_sonarqube_when_not_on_SonarCloud() { | |||
assertThat(underTest.getInstanceName()).isEqualTo("SonarQube"); | |||
} | |||
@Test | |||
public void getInstanceName_returns_sonarcloud_on_SonarCloud() { | |||
settings.setProperty("sonar.sonarcloud.enabled", true); | |||
assertThat(underTest.getInstanceName()).isEqualTo("SonarCloud"); | |||
} | |||
@Test | |||
public void return_definitions() { | |||
assertThat(EmailSettings.definitions()).hasSize(8); |