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;
WebhookPostTask.class,
// notifications
- NewIssuesNotificationFactory.class);
+ NotificationFactory.class);
}
}
+++ /dev/null
-/*
- * 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.ce.task.projectanalysis.notification;
-
-import com.google.common.collect.ImmutableMap;
-import java.util.Map;
-import java.util.Optional;
-import org.sonar.api.ce.ComputeEngineSide;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.utils.Durations;
-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.db.user.UserDto;
-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.Objects.requireNonNull;
-import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.PRE_ORDER;
-import static org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit.FILE;
-
-@ComputeEngineSide
-public class NewIssuesNotificationFactory {
- private final TreeRootHolder treeRootHolder;
- private final RuleRepository ruleRepository;
- private final Durations durations;
- private Map<String, Component> componentsByUuid;
-
- public NewIssuesNotificationFactory(TreeRootHolder treeRootHolder, RuleRepository ruleRepository, Durations durations) {
- this.treeRootHolder = treeRootHolder;
- this.ruleRepository = ruleRepository;
- this.durations = durations;
- }
-
- public MyNewIssuesNotification newMyNewIssuesNotification(Map<String, UserDto> assigneesByUuid) {
- verifyAssigneesByUuid(assigneesByUuid);
- return new MyNewIssuesNotification(durations, new DetailsSupplierImpl(assigneesByUuid));
- }
-
- public NewIssuesNotification newNewIssuesNotification(Map<String, UserDto> assigneesByUuid) {
- verifyAssigneesByUuid(assigneesByUuid);
- return new NewIssuesNotification(durations, new DetailsSupplierImpl(assigneesByUuid));
- }
-
- private static void verifyAssigneesByUuid(Map<String, UserDto> assigneesByUuid) {
- requireNonNull(assigneesByUuid, "assigneesByUuid can't be null");
- }
-
- private class DetailsSupplierImpl implements DetailsSupplier {
- private final Map<String, UserDto> assigneesByUuid;
-
- private DetailsSupplierImpl(Map<String, UserDto> assigneesByUuid) {
- this.assigneesByUuid = assigneesByUuid;
- }
-
- @Override
- public Optional<RuleDefinition> getRuleDefinitionByRuleKey(RuleKey ruleKey) {
- requireNonNull(ruleKey, "ruleKey can't be null");
- return ruleRepository.findByKey(ruleKey)
- .map(t -> new RuleDefinition(t.getName(), t.getLanguage()));
- }
-
- @Override
- public Optional<String> getComponentNameByUuid(String uuid) {
- requireNonNull(uuid, "uuid can't be null");
- return Optional.ofNullable(lazyLoadComponentsByUuid().get(uuid))
- .map(t -> t.getType() == Component.Type.FILE || t.getType() == Component.Type.DIRECTORY ? t.getShortName() : t.getName());
- }
-
- private Map<String, Component> lazyLoadComponentsByUuid() {
- if (componentsByUuid == null) {
- ImmutableMap.Builder<String, Component> builder = ImmutableMap.builder();
- new DepthTraversalTypeAwareCrawler(new TypeAwareVisitorAdapter(FILE, PRE_ORDER) {
- @Override
- public void visitAny(Component any) {
- builder.put(any.getUuid(), any);
- }
- }).visit(treeRootHolder.getRoot());
- componentsByUuid = builder.build();
- }
- return componentsByUuid;
- }
-
- @Override
- public Optional<String> getUserNameByUuid(String uuid) {
- requireNonNull(uuid, "uuid can't be null");
- return Optional.ofNullable(assigneesByUuid.get(uuid))
- .map(UserDto::getName);
- }
- }
-}
--- /dev/null
+/*
+ * 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.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 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 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) {
+ verifyAssigneesByUuid(assigneesByUuid);
+ return new MyNewIssuesNotification(durations, new DetailsSupplierImpl(assigneesByUuid));
+ }
+
+ public NewIssuesNotification newNewIssuesNotification(Map<String, UserDto> assigneesByUuid) {
+ verifyAssigneesByUuid(assigneesByUuid);
+ 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");
+ }
+
+ private class DetailsSupplierImpl implements DetailsSupplier {
+ private final Map<String, UserDto> assigneesByUuid;
+
+ private DetailsSupplierImpl(Map<String, UserDto> assigneesByUuid) {
+ this.assigneesByUuid = assigneesByUuid;
+ }
+
+ @Override
+ public Optional<RuleDefinition> getRuleDefinitionByRuleKey(RuleKey ruleKey) {
+ requireNonNull(ruleKey, "ruleKey can't be null");
+ return ruleRepository.findByKey(ruleKey)
+ .map(t -> new RuleDefinition(t.getName(), t.getLanguage()));
+ }
+
+ @Override
+ public Optional<String> getComponentNameByUuid(String uuid) {
+ requireNonNull(uuid, "uuid can't be null");
+ return Optional.ofNullable(lazyLoadComponentsByUuid().get(uuid))
+ .map(t -> t.getType() == Component.Type.FILE || t.getType() == Component.Type.DIRECTORY ? t.getShortName() : t.getName());
+ }
+
+ private Map<String, Component> lazyLoadComponentsByUuid() {
+ if (componentsByUuid == null) {
+ ImmutableMap.Builder<String, Component> builder = ImmutableMap.builder();
+ new DepthTraversalTypeAwareCrawler(new TypeAwareVisitorAdapter(FILE, PRE_ORDER) {
+ @Override
+ public void visitAny(Component any) {
+ builder.put(any.getUuid(), any);
+ }
+ }).visit(treeRootHolder.getRoot());
+ componentsByUuid = builder.build();
+ }
+ return componentsByUuid;
+ }
+
+ @Override
+ public Optional<String> getUserNameByUuid(String uuid) {
+ requireNonNull(uuid, "uuid can't be null");
+ return Optional.ofNullable(assigneesByUuid.get(uuid))
+ .map(UserDto::getName);
+ }
+ }
+}
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;
}
@Override
+ @CheckForNull
public EmailMessage format(Notification notification) {
if (!(notification instanceof ReportAnalysisFailureNotification)) {
return null;
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) {
*/
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;
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;
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;
/**
* 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;
}
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);
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())
.map(e -> {
String assigneeUuid = e.getKey();
NewIssuesStatistics.Stats assigneeStatistics = e.getValue();
- MyNewIssuesNotification myNewIssuesNotification = newIssuesNotificationFactory
+ MyNewIssuesNotification myNewIssuesNotification = notificationFactory
.newMyNewIssuesNotification(assigneesByUuid)
.setAssignee(userDtoByUuid.get(assigneeUuid));
myNewIssuesNotification
return myNewIssuesNotification;
})
- .collect(MoreCollectors.toSet(statistics.getAssigneesStatistics().size()));
+ .collect(toSet(statistics.getAssigneesStatistics().size()));
notificationStatistics.myNewIssuesDeliveries += service.deliverEmails(myNewIssuesNotifications);
notificationStatistics.myNewIssues += myNewIssuesNotifications.size();
}
}
- 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";
public DumbRule(RuleKey key) {
this.key = key;
this.id = key.hashCode();
+ this.name = "name_" + key;
}
@Override
+++ /dev/null
-/*
- * 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.ce.task.projectanalysis.notification;
-
-import com.google.common.collect.ImmutableMap;
-import java.lang.reflect.Field;
-import java.util.Random;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.utils.Durations;
-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.db.user.UserDto;
-import org.sonar.db.user.UserTesting;
-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.assertj.core.api.Assertions.assertThat;
-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 {
- @Rule
- public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
- @Rule
- public RuleRepositoryRule ruleRepository = new RuleRepositoryRule();
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- private Durations durations = new Durations();
- private NewIssuesNotificationFactory underTest = new NewIssuesNotificationFactory(treeRootHolder, ruleRepository, durations);
-
- @Test
- public void newMyNewIssuesNotification_throws_NPE_if_assigneesByUuid_is_null() {
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("assigneesByUuid can't be null");
-
- underTest.newMyNewIssuesNotification(null);
- }
-
- @Test
- public void newNewIssuesNotification_throws_NPE_if_assigneesByUuid_is_null() {
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("assigneesByUuid can't be null");
-
- underTest.newNewIssuesNotification(null);
- }
-
- @Test
- public void newMyNewIssuesNotification_returns_MyNewIssuesNotification_object_with_the_constructor_Durations() {
- MyNewIssuesNotification notification = underTest.newMyNewIssuesNotification(emptyMap());
-
- assertThat(readDurationsField(notification)).isSameAs(durations);
- }
-
- @Test
- public void newNewIssuesNotification_returns_NewIssuesNotification_object_with_the_constructor_Durations() {
- NewIssuesNotification notification = underTest.newNewIssuesNotification(emptyMap());
-
- assertThat(readDurationsField(notification)).isSameAs(durations);
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getUserNameByUuid_fails_with_NPE_if_uuid_is_null() {
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("uuid can't be null");
-
- detailsSupplier.getUserNameByUuid(null);
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getUserNameByUuid_always_returns_empty_if_map_argument_is_empty() {
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
- assertThat(detailsSupplier.getUserNameByUuid("foo")).isEmpty();
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getUserNameByUuid_returns_name_of_user_from_map_argument() {
- Set<UserDto> users = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> UserTesting.newUserDto().setLogin("user" + i))
- .collect(Collectors.toSet());
-
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(
- users.stream().collect(uniqueIndex(UserDto::getUuid)));
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
- assertThat(detailsSupplier.getUserNameByUuid("foo")).isEmpty();
- users
- .forEach(user -> assertThat(detailsSupplier.getUserNameByUuid(user.getUuid())).contains(user.getName()));
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getUserNameByUuid_returns_empty_if_user_has_null_name() {
- UserDto user = UserTesting.newUserDto().setLogin("user_noname").setName(null);
-
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(ImmutableMap.of(user.getUuid(), user));
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
- assertThat(detailsSupplier.getUserNameByUuid(user.getUuid())).isEmpty();
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getUserNameByUuid_fails_with_NPE_if_uuid_is_null() {
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("uuid can't be null");
-
- detailsSupplier.getUserNameByUuid(null);
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getUserNameByUuid_always_returns_empty_if_map_argument_is_empty() {
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
- assertThat(detailsSupplier.getUserNameByUuid("foo")).isEmpty();
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getUserNameByUuid_returns_name_of_user_from_map_argument() {
- Set<UserDto> users = IntStream.range(0, 1 + new Random().nextInt(10))
- .mapToObj(i -> UserTesting.newUserDto().setLogin("user" + i))
- .collect(Collectors.toSet());
-
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(
- users.stream().collect(uniqueIndex(UserDto::getUuid)));
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
- assertThat(detailsSupplier.getUserNameByUuid("foo")).isEmpty();
- users
- .forEach(user -> assertThat(detailsSupplier.getUserNameByUuid(user.getUuid())).contains(user.getName()));
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getUserNameByUuid_returns_empty_if_user_has_null_name() {
- UserDto user = UserTesting.newUserDto().setLogin("user_noname").setName(null);
-
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(ImmutableMap.of(user.getUuid(), user));
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
- assertThat(detailsSupplier.getUserNameByUuid(user.getUuid())).isEmpty();
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_fails_with_ISE_if_TreeRootHolder_is_not_initialized() {
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Holder has not been initialized yet");
-
- detailsSupplier.getComponentNameByUuid("foo");
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_fails_with_NPE_if_uuid_is_null() {
- treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root").build());
-
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("uuid can't be null");
-
- detailsSupplier.getComponentNameByUuid(null);
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_returns_name_of_project_in_TreeRootHolder() {
- treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root").build());
-
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- assertThat(detailsSupplier.getComponentNameByUuid("rootUuid")).contains("root");
- assertThat(detailsSupplier.getComponentNameByUuid("foo")).isEmpty();
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_returns_shortName_of_dir_and_file_in_TreeRootHolder() {
- treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root")
- .addChildren(ReportComponent.builder(DIRECTORY, 2).setUuid("dir1Uuid").setName("dir1").setShortName("dir1_short")
- .addChildren(ReportComponent.builder(FILE, 21).setUuid("file21Uuid").setName("file21").setShortName("file21_short").build())
- .build())
- .addChildren(ReportComponent.builder(DIRECTORY, 3).setUuid("dir2Uuid").setName("dir2").setShortName("dir2_short")
- .addChildren(ReportComponent.builder(FILE, 31).setUuid("file31Uuid").setName("file31").setShortName("file31_short").build())
- .addChildren(ReportComponent.builder(FILE, 32).setUuid("file32Uuid").setName("file32").setShortName("file32_short").build())
- .build())
- .addChildren(ReportComponent.builder(FILE, 11).setUuid("file11Uuid").setName("file11").setShortName("file11_short").build())
- .build());
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- Stream.of("dir1", "dir2", "file11", "file21", "file31", "file32")
- .forEach(name -> {
- assertThat(detailsSupplier.getComponentNameByUuid(name + "Uuid")).contains(name + "_short");
- assertThat(detailsSupplier.getComponentNameByUuid(name)).isEmpty();
- });
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_fails_with_ISE_if_TreeRootHolder_is_not_initialized() {
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Holder has not been initialized yet");
-
- detailsSupplier.getComponentNameByUuid("foo");
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_fails_with_NPE_if_uuid_is_null() {
- treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root").build());
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("uuid can't be null");
-
- detailsSupplier.getComponentNameByUuid(null);
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_returns_name_of_project_in_TreeRootHolder() {
- treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root").build());
-
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- assertThat(detailsSupplier.getComponentNameByUuid("rootUuid")).contains("root");
- assertThat(detailsSupplier.getComponentNameByUuid("foo")).isEmpty();
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_returns_shortName_of_dir_and_file_in_TreeRootHolder() {
- treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root")
- .addChildren(ReportComponent.builder(DIRECTORY, 2).setUuid("dir1Uuid").setName("dir1").setShortName("dir1_short")
- .addChildren(ReportComponent.builder(FILE, 21).setUuid("file21Uuid").setName("file21").setShortName("file21_short").build())
- .build())
- .addChildren(ReportComponent.builder(DIRECTORY, 3).setUuid("dir2Uuid").setName("dir2").setShortName("dir2_short")
- .addChildren(ReportComponent.builder(FILE, 31).setUuid("file31Uuid").setName("file31").setShortName("file31_short").build())
- .addChildren(ReportComponent.builder(FILE, 32).setUuid("file32Uuid").setName("file32").setShortName("file32_short").build())
- .build())
- .addChildren(ReportComponent.builder(FILE, 11).setUuid("file11Uuid").setName("file11").setShortName("file11_short").build())
- .build());
-
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- Stream.of("dir1", "dir2", "file11", "file21", "file31", "file32")
- .forEach(name -> {
- assertThat(detailsSupplier.getComponentNameByUuid(name + "Uuid")).contains(name + "_short");
- assertThat(detailsSupplier.getComponentNameByUuid(name)).isEmpty();
- });
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_fails_with_NPE_if_ruleKey_is_null() {
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("ruleKey can't be null");
-
- detailsSupplier.getRuleDefinitionByRuleKey(null);
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_always_returns_empty_if_RuleRepository_is_empty() {
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("foo", "bar"))).isEmpty();
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("bar", "foo"))).isEmpty();
- }
-
- @Test
- public void newMyNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_returns_name_and_language_from_RuleRepository() {
- RuleKey rulekey1 = RuleKey.of("foo", "bar");
- RuleKey rulekey2 = RuleKey.of("foo", "donut");
- RuleKey rulekey3 = RuleKey.of("no", "language");
- DumbRule rule1 = ruleRepository.add(rulekey1).setName("rule1").setLanguage("lang1");
- DumbRule rule2 = ruleRepository.add(rulekey2).setName("rule2").setLanguage("lang2");
- DumbRule rule3 = ruleRepository.add(rulekey3).setName("rule3");
-
- MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey1))
- .contains(new RuleDefinition(rule1.getName(), rule1.getLanguage()));
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey2))
- .contains(new RuleDefinition(rule2.getName(), rule2.getLanguage()));
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey3))
- .contains(new RuleDefinition(rule3.getName(), null));
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("donut", "foo")))
- .isEmpty();
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_fails_with_NPE_if_ruleKey_is_null() {
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- expectedException.expect(NullPointerException.class);
- expectedException.expectMessage("ruleKey can't be null");
-
- detailsSupplier.getRuleDefinitionByRuleKey(null);
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_always_returns_empty_if_RuleRepository_is_empty() {
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("foo", "bar"))).isEmpty();
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("bar", "foo"))).isEmpty();
- }
-
- @Test
- public void newNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_returns_name_and_language_from_RuleRepository() {
- RuleKey rulekey1 = RuleKey.of("foo", "bar");
- RuleKey rulekey2 = RuleKey.of("foo", "donut");
- RuleKey rulekey3 = RuleKey.of("no", "language");
- DumbRule rule1 = ruleRepository.add(rulekey1).setName("rule1").setLanguage("lang1");
- DumbRule rule2 = ruleRepository.add(rulekey2).setName("rule2").setLanguage("lang2");
- DumbRule rule3 = ruleRepository.add(rulekey3).setName("rule3");
-
- NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
-
- DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
-
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey1))
- .contains(new RuleDefinition(rule1.getName(), rule1.getLanguage()));
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey2))
- .contains(new RuleDefinition(rule2.getName(), rule2.getLanguage()));
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey3))
- .contains(new RuleDefinition(rule3.getName(), null));
- assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("donut", "foo")))
- .isEmpty();
- }
-
- private static Durations readDurationsField(NewIssuesNotification notification) {
- return readField(notification, "durations");
- }
-
- private static Durations readField(NewIssuesNotification notification, String fieldName) {
- try {
- Field durationsField = NewIssuesNotification.class.getDeclaredField(fieldName);
- durationsField.setAccessible(true);
- Object o = durationsField.get(notification);
- return (Durations) o;
- } catch (IllegalAccessException | NoSuchFieldException e) {
- throw new RuntimeException(e);
- }
- }
-
- private static DetailsSupplier readDetailsSupplier(NewIssuesNotification notification) {
- try {
- Field durationsField = NewIssuesNotification.class.getDeclaredField("detailsSupplier");
- durationsField.setAccessible(true);
- return (DetailsSupplier) durationsField.get(notification);
- } catch (IllegalAccessException | NoSuchFieldException e) {
- throw new RuntimeException(e);
- }
- }
-}
--- /dev/null
+/*
+ * 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.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;
+import java.util.stream.IntStream;
+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;
+
+@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 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() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("assigneesByUuid can't be null");
+
+ underTest.newMyNewIssuesNotification(null);
+ }
+
+ @Test
+ public void newNewIssuesNotification_throws_NPE_if_assigneesByUuid_is_null() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("assigneesByUuid can't be null");
+
+ underTest.newNewIssuesNotification(null);
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_returns_MyNewIssuesNotification_object_with_the_constructor_Durations() {
+ MyNewIssuesNotification notification = underTest.newMyNewIssuesNotification(emptyMap());
+
+ assertThat(readDurationsField(notification)).isSameAs(durations);
+ }
+
+ @Test
+ public void newNewIssuesNotification_returns_NewIssuesNotification_object_with_the_constructor_Durations() {
+ NewIssuesNotification notification = underTest.newNewIssuesNotification(emptyMap());
+
+ assertThat(readDurationsField(notification)).isSameAs(durations);
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getUserNameByUuid_fails_with_NPE_if_uuid_is_null() {
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("uuid can't be null");
+
+ detailsSupplier.getUserNameByUuid(null);
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getUserNameByUuid_always_returns_empty_if_map_argument_is_empty() {
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+ assertThat(detailsSupplier.getUserNameByUuid("foo")).isEmpty();
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getUserNameByUuid_returns_name_of_user_from_map_argument() {
+ Set<UserDto> users = IntStream.range(0, 1 + new Random().nextInt(10))
+ .mapToObj(i -> UserTesting.newUserDto().setLogin("user" + i))
+ .collect(Collectors.toSet());
+
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(
+ users.stream().collect(uniqueIndex(UserDto::getUuid)));
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+ assertThat(detailsSupplier.getUserNameByUuid("foo")).isEmpty();
+ users
+ .forEach(user -> assertThat(detailsSupplier.getUserNameByUuid(user.getUuid())).contains(user.getName()));
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getUserNameByUuid_returns_empty_if_user_has_null_name() {
+ UserDto user = UserTesting.newUserDto().setLogin("user_noname").setName(null);
+
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(ImmutableMap.of(user.getUuid(), user));
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+ assertThat(detailsSupplier.getUserNameByUuid(user.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getUserNameByUuid_fails_with_NPE_if_uuid_is_null() {
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("uuid can't be null");
+
+ detailsSupplier.getUserNameByUuid(null);
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getUserNameByUuid_always_returns_empty_if_map_argument_is_empty() {
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+ assertThat(detailsSupplier.getUserNameByUuid("foo")).isEmpty();
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getUserNameByUuid_returns_name_of_user_from_map_argument() {
+ Set<UserDto> users = IntStream.range(0, 1 + new Random().nextInt(10))
+ .mapToObj(i -> UserTesting.newUserDto().setLogin("user" + i))
+ .collect(Collectors.toSet());
+
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(
+ users.stream().collect(uniqueIndex(UserDto::getUuid)));
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+ assertThat(detailsSupplier.getUserNameByUuid("foo")).isEmpty();
+ users
+ .forEach(user -> assertThat(detailsSupplier.getUserNameByUuid(user.getUuid())).contains(user.getName()));
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getUserNameByUuid_returns_empty_if_user_has_null_name() {
+ UserDto user = UserTesting.newUserDto().setLogin("user_noname").setName(null);
+
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(ImmutableMap.of(user.getUuid(), user));
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+ assertThat(detailsSupplier.getUserNameByUuid(user.getUuid())).isEmpty();
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_fails_with_ISE_if_TreeRootHolder_is_not_initialized() {
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Holder has not been initialized yet");
+
+ detailsSupplier.getComponentNameByUuid("foo");
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_fails_with_NPE_if_uuid_is_null() {
+ treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root").build());
+
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("uuid can't be null");
+
+ detailsSupplier.getComponentNameByUuid(null);
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_returns_name_of_project_in_TreeRootHolder() {
+ treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root").build());
+
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ assertThat(detailsSupplier.getComponentNameByUuid("rootUuid")).contains("root");
+ assertThat(detailsSupplier.getComponentNameByUuid("foo")).isEmpty();
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_returns_shortName_of_dir_and_file_in_TreeRootHolder() {
+ treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root")
+ .addChildren(ReportComponent.builder(DIRECTORY, 2).setUuid("dir1Uuid").setName("dir1").setShortName("dir1_short")
+ .addChildren(ReportComponent.builder(FILE, 21).setUuid("file21Uuid").setName("file21").setShortName("file21_short").build())
+ .build())
+ .addChildren(ReportComponent.builder(DIRECTORY, 3).setUuid("dir2Uuid").setName("dir2").setShortName("dir2_short")
+ .addChildren(ReportComponent.builder(FILE, 31).setUuid("file31Uuid").setName("file31").setShortName("file31_short").build())
+ .addChildren(ReportComponent.builder(FILE, 32).setUuid("file32Uuid").setName("file32").setShortName("file32_short").build())
+ .build())
+ .addChildren(ReportComponent.builder(FILE, 11).setUuid("file11Uuid").setName("file11").setShortName("file11_short").build())
+ .build());
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ Stream.of("dir1", "dir2", "file11", "file21", "file31", "file32")
+ .forEach(name -> {
+ assertThat(detailsSupplier.getComponentNameByUuid(name + "Uuid")).contains(name + "_short");
+ assertThat(detailsSupplier.getComponentNameByUuid(name)).isEmpty();
+ });
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_fails_with_ISE_if_TreeRootHolder_is_not_initialized() {
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Holder has not been initialized yet");
+
+ detailsSupplier.getComponentNameByUuid("foo");
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_fails_with_NPE_if_uuid_is_null() {
+ treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root").build());
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("uuid can't be null");
+
+ detailsSupplier.getComponentNameByUuid(null);
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_returns_name_of_project_in_TreeRootHolder() {
+ treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root").build());
+
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ assertThat(detailsSupplier.getComponentNameByUuid("rootUuid")).contains("root");
+ assertThat(detailsSupplier.getComponentNameByUuid("foo")).isEmpty();
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getComponentNameByUuid_returns_shortName_of_dir_and_file_in_TreeRootHolder() {
+ treeRootHolder.setRoot(ReportComponent.builder(PROJECT, 1).setUuid("rootUuid").setName("root")
+ .addChildren(ReportComponent.builder(DIRECTORY, 2).setUuid("dir1Uuid").setName("dir1").setShortName("dir1_short")
+ .addChildren(ReportComponent.builder(FILE, 21).setUuid("file21Uuid").setName("file21").setShortName("file21_short").build())
+ .build())
+ .addChildren(ReportComponent.builder(DIRECTORY, 3).setUuid("dir2Uuid").setName("dir2").setShortName("dir2_short")
+ .addChildren(ReportComponent.builder(FILE, 31).setUuid("file31Uuid").setName("file31").setShortName("file31_short").build())
+ .addChildren(ReportComponent.builder(FILE, 32).setUuid("file32Uuid").setName("file32").setShortName("file32_short").build())
+ .build())
+ .addChildren(ReportComponent.builder(FILE, 11).setUuid("file11Uuid").setName("file11").setShortName("file11_short").build())
+ .build());
+
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ Stream.of("dir1", "dir2", "file11", "file21", "file31", "file32")
+ .forEach(name -> {
+ assertThat(detailsSupplier.getComponentNameByUuid(name + "Uuid")).contains(name + "_short");
+ assertThat(detailsSupplier.getComponentNameByUuid(name)).isEmpty();
+ });
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_fails_with_NPE_if_ruleKey_is_null() {
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("ruleKey can't be null");
+
+ detailsSupplier.getRuleDefinitionByRuleKey(null);
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_always_returns_empty_if_RuleRepository_is_empty() {
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("foo", "bar"))).isEmpty();
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("bar", "foo"))).isEmpty();
+ }
+
+ @Test
+ public void newMyNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_returns_name_and_language_from_RuleRepository() {
+ RuleKey rulekey1 = RuleKey.of("foo", "bar");
+ RuleKey rulekey2 = RuleKey.of("foo", "donut");
+ RuleKey rulekey3 = RuleKey.of("no", "language");
+ DumbRule rule1 = ruleRepository.add(rulekey1).setName("rule1").setLanguage("lang1");
+ DumbRule rule2 = ruleRepository.add(rulekey2).setName("rule2").setLanguage("lang2");
+ DumbRule rule3 = ruleRepository.add(rulekey3).setName("rule3");
+
+ MyNewIssuesNotification underTest = this.underTest.newMyNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey1))
+ .contains(new RuleDefinition(rule1.getName(), rule1.getLanguage()));
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey2))
+ .contains(new RuleDefinition(rule2.getName(), rule2.getLanguage()));
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey3))
+ .contains(new RuleDefinition(rule3.getName(), null));
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("donut", "foo")))
+ .isEmpty();
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_fails_with_NPE_if_ruleKey_is_null() {
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("ruleKey can't be null");
+
+ detailsSupplier.getRuleDefinitionByRuleKey(null);
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_always_returns_empty_if_RuleRepository_is_empty() {
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("foo", "bar"))).isEmpty();
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("bar", "foo"))).isEmpty();
+ }
+
+ @Test
+ public void newNewIssuesNotification_DetailsSupplier_getRuleDefinitionByRuleKey_returns_name_and_language_from_RuleRepository() {
+ RuleKey rulekey1 = RuleKey.of("foo", "bar");
+ RuleKey rulekey2 = RuleKey.of("foo", "donut");
+ RuleKey rulekey3 = RuleKey.of("no", "language");
+ DumbRule rule1 = ruleRepository.add(rulekey1).setName("rule1").setLanguage("lang1");
+ DumbRule rule2 = ruleRepository.add(rulekey2).setName("rule2").setLanguage("lang2");
+ DumbRule rule3 = ruleRepository.add(rulekey3).setName("rule3");
+
+ NewIssuesNotification underTest = this.underTest.newNewIssuesNotification(emptyMap());
+
+ DetailsSupplier detailsSupplier = readDetailsSupplier(underTest);
+
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey1))
+ .contains(new RuleDefinition(rule1.getName(), rule1.getLanguage()));
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey2))
+ .contains(new RuleDefinition(rule2.getName(), rule2.getLanguage()));
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(rulekey3))
+ .contains(new RuleDefinition(rule3.getName(), null));
+ assertThat(detailsSupplier.getRuleDefinitionByRuleKey(RuleKey.of("donut", "foo")))
+ .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");
+ }
+
+ private static Durations readField(NewIssuesNotification notification, String fieldName) {
+ try {
+ Field durationsField = NewIssuesNotification.class.getDeclaredField(fieldName);
+ durationsField.setAccessible(true);
+ Object o = durationsField.get(notification);
+ return (Durations) o;
+ } catch (IllegalAccessException | NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static DetailsSupplier readDetailsSupplier(NewIssuesNotification notification) {
+ try {
+ Field durationsField = NewIssuesNotification.class.getDeclaredField("detailsSupplier");
+ durationsField.setAccessible(true);
+ return (DetailsSupplier) durationsField.get(notification);
+ } catch (IllegalAccessException | NoSuchFieldException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
*/
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;
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;
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;
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;
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;
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;
.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);
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();
@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
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));
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<>();
// 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));
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);
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;
.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() {
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;
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,
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
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;
}
@Override
+ @CheckForNull
public EmailMessage format(Notification notification) {
if (shouldNotFormat(notification)) {
return null;
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) {
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;
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)
.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
}
@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());
+ }
+
}
--- /dev/null
+/*
+ * 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";
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
+++ /dev/null
-/*
- * 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)));
- }
-
-}
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
/**
* @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;
return messageId;
}
+ public boolean isHtml() {
+ return html;
+ }
+
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
*/
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;
@ExtensionPoint
public interface EmailTemplate {
+ @CheckForNull
EmailMessage format(Notification notification);
}
--- /dev/null
+/*
+ * 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 +
+ '}';
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
+++ /dev/null
-/*
- * 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);
- }
-}
*/
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);
}
}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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 +
+ '}';
+ }
+ }
+}
--- /dev/null
+/*
+ * 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
+ );
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
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;
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 {
}
}
- 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);
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);
*/
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;
}
@Override
+ @CheckForNull
public EmailMessage format(Notification notification) {
if (!"alerts".equals(notification.getType())) {
return null;
return new EmailMessage()
.setMessageId("alerts/" + projectId)
.setSubject(subject)
- .setMessage(messageBody);
+ .setPlainTextMessage(messageBody);
}
private static String computeFullProjectName(String projectName, @Nullable String branchName) {
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;
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;
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() {
@Test
public void getNotificationClass_is_IssueChangeNotification() {
- assertThat(underTest.getNotificationClass()).isEqualTo(IssueChangeNotification.class);
+ assertThat(underTest.getNotificationClass()).isEqualTo(IssuesChangesNotification.class);
}
@Test
@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);
}
@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);
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;
}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+
+}
+++ /dev/null
-/*
- * 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";
- }
-
-}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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";
+ }
+
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
+++ /dev/null
-/*
- * 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");
- }
-}
+++ /dev/null
-/*
- * 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");
- }
-}
--- /dev/null
+/*
+ * 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());
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+
+}
--- /dev/null
+/*
+ * 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");
+ }
+
+}
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;
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)));
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;
}
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);
}
}
}
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);
}
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;
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;
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;
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;
this.notificationService = notificationService;
this.actions = actions;
this.issueChangePostProcessor = issueChangePostProcessor;
+ this.notificationSerializer = notificationSerializer;
}
@Override
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;
}
}
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()));
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())
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);
}
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);
}
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);
}
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);
}
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;
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;
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,
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;
}
@Override
+ @CheckForNull
public EmailMessage format(Notification notification) {
if (!BuiltInQPChangeNotification.TYPE.equals(notification.getType())) {
return null;
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) {
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;
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 {
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();
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
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());
}
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
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);
}
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);
}
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
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();
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
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);
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);
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;
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;
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);
}
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());
}
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;
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);
}
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());
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;
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;
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 {
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() {
.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();
.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
.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
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;
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);
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");
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;
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;
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
}
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);
}
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;
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;
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
}
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) {
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;
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
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);
}
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();
.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();
EmailMessage emailMessage = new EmailMessage()
.setTo("user@nowhere")
.setSubject("Foo")
- .setMessage("Bar");
+ .setPlainTextMessage("Bar");
boolean delivered = underTest.deliver(emailMessage);
List<WiserMessage> messages = smtpServer.getMessages();
EmailMessage emailMessage = new EmailMessage()
.setTo("user@nowhere")
.setSubject("Foo")
- .setMessage("Bar");
+ .setPlainTextMessage("Bar");
boolean delivered = underTest.deliver(emailMessage);
assertThat(delivered).isFalse();
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)
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);
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;
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")
},
underTest.start();
assertThat(underTest.getProjectDispatchers()).containsExactly(
- QGChangeNotificationHandler.KEY, DoNotFixNotificationHandler.KEY, MyNewIssuesNotificationHandler.KEY);
+ QGChangeNotificationHandler.KEY, FPOrWontFixNotificationHandler.KEY, MyNewIssuesNotificationHandler.KEY);
}
@Test
underTest.start();
assertThat(underTest.getProjectDispatchers()).containsOnly(
- MyNewIssuesNotificationHandler.KEY, QGChangeNotificationHandler.KEY, DoNotFixNotificationHandler.KEY);
+ MyNewIssuesNotificationHandler.KEY, QGChangeNotificationHandler.KEY, FPOrWontFixNotificationHandler.KEY);
}
}
ensureAbstractMockUserSession().addOrganizationMembership(organization);
return this;
}
-
}
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;
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
*/
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);
};
*/
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);
};
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.
*
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;
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;
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 {
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();
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);
@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);
}
@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();
}
@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();
}
@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();
}
}
+ 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
}
.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);
}
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);