547 lines
19 KiB
Java
547 lines
19 KiB
Java
// Copyright (C) 2017 The Android Open Source Project
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package com.google.gerrit.acceptance;
|
|
|
|
import static com.google.common.truth.Fact.fact;
|
|
import static com.google.common.truth.Truth.assertAbout;
|
|
import static com.google.gerrit.extensions.api.changes.RecipientType.BCC;
|
|
import static com.google.gerrit.extensions.api.changes.RecipientType.CC;
|
|
import static com.google.gerrit.extensions.api.changes.RecipientType.TO;
|
|
import static java.util.stream.Collectors.toList;
|
|
|
|
import com.google.common.base.Joiner;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.truth.FailureMetadata;
|
|
import com.google.common.truth.Subject;
|
|
import com.google.common.truth.Truth;
|
|
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
|
|
import com.google.gerrit.common.Nullable;
|
|
import com.google.gerrit.extensions.api.changes.RecipientType;
|
|
import com.google.gerrit.extensions.api.changes.ReviewInput;
|
|
import com.google.gerrit.extensions.api.changes.ReviewResult;
|
|
import com.google.gerrit.extensions.api.projects.ConfigInput;
|
|
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
|
|
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
|
|
import com.google.gerrit.extensions.client.InheritableBoolean;
|
|
import com.google.gerrit.extensions.client.ReviewerState;
|
|
import com.google.gerrit.mail.Address;
|
|
import com.google.gerrit.mail.EmailHeader;
|
|
import com.google.gerrit.mail.EmailHeader.AddressList;
|
|
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
|
|
import com.google.gerrit.testing.FakeEmailSender;
|
|
import com.google.gerrit.testing.FakeEmailSender.Message;
|
|
import com.google.inject.Inject;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.function.Function;
|
|
import org.eclipse.jgit.junit.TestRepository;
|
|
import org.junit.After;
|
|
import org.junit.Before;
|
|
|
|
public abstract class AbstractNotificationTest extends AbstractDaemonTest {
|
|
@Inject private RequestScopeOperations requestScopeOperations;
|
|
|
|
@Before
|
|
public void enableReviewerByEmail() throws Exception {
|
|
requestScopeOperations.setApiUser(admin.id());
|
|
ConfigInput conf = new ConfigInput();
|
|
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
|
|
gApi.projects().name(project.get()).config(conf);
|
|
}
|
|
|
|
@Override
|
|
protected ProjectResetter.Config resetProjects() {
|
|
// Don't reset anything so that stagedUsers can be cached across all tests.
|
|
// Without this caching these tests become much too slow.
|
|
return new ProjectResetter.Config();
|
|
}
|
|
|
|
protected static FakeEmailSenderSubject assertThat(FakeEmailSender sender) {
|
|
return assertAbout(fakeEmailSenders()).that(sender);
|
|
}
|
|
|
|
protected static Subject.Factory<FakeEmailSenderSubject, FakeEmailSender> fakeEmailSenders() {
|
|
return FakeEmailSenderSubject::new;
|
|
}
|
|
|
|
protected void setEmailStrategy(TestAccount account, EmailStrategy strategy) throws Exception {
|
|
setEmailStrategy(account, strategy, true);
|
|
}
|
|
|
|
protected void setEmailStrategy(TestAccount account, EmailStrategy strategy, boolean record)
|
|
throws Exception {
|
|
if (record) {
|
|
accountsModifyingEmailStrategy.add(account);
|
|
}
|
|
requestScopeOperations.setApiUser(account.id());
|
|
GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences();
|
|
prefs.emailStrategy = strategy;
|
|
gApi.accounts().self().setPreferences(prefs);
|
|
}
|
|
|
|
protected static class FakeEmailSenderSubject extends Subject {
|
|
private final FakeEmailSender fakeEmailSender;
|
|
private Message message;
|
|
private StagedUsers users;
|
|
private Map<RecipientType, List<String>> recipients = new HashMap<>();
|
|
private Set<String> accountedFor = new HashSet<>();
|
|
|
|
FakeEmailSenderSubject(FailureMetadata failureMetadata, FakeEmailSender target) {
|
|
super(failureMetadata, target);
|
|
fakeEmailSender = target;
|
|
}
|
|
|
|
public FakeEmailSenderSubject didNotSend() {
|
|
Message message = fakeEmailSender.peekMessage();
|
|
if (message != null) {
|
|
failWithoutActual(fact("expected no message", message));
|
|
}
|
|
return this;
|
|
}
|
|
|
|
public FakeEmailSenderSubject sent(String messageType, StagedUsers users) {
|
|
message = fakeEmailSender.nextMessage();
|
|
if (message == null) {
|
|
failWithoutActual(fact("expected message", "not sent"));
|
|
}
|
|
recipients = new HashMap<>();
|
|
recipients.put(TO, parseAddresses(message, "To"));
|
|
recipients.put(CC, parseAddresses(message, "Cc"));
|
|
recipients.put(
|
|
BCC,
|
|
message.rcpt().stream()
|
|
.map(Address::getEmail)
|
|
.filter(e -> !recipients.get(TO).contains(e) && !recipients.get(CC).contains(e))
|
|
.collect(toList()));
|
|
this.users = users;
|
|
if (!message.headers().containsKey("X-Gerrit-MessageType")) {
|
|
failWithoutActual(
|
|
fact("expected to have message sent with", "X-Gerrit-MessageType header"));
|
|
}
|
|
EmailHeader header = message.headers().get("X-Gerrit-MessageType");
|
|
if (!header.equals(new EmailHeader.String(messageType))) {
|
|
failWithoutActual(
|
|
fact("expected message of type", messageType),
|
|
fact(
|
|
"actual",
|
|
header instanceof EmailHeader.String
|
|
? ((EmailHeader.String) header).getString()
|
|
: header));
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
private static String recipientMapToString(
|
|
Map<RecipientType, List<String>> recipients, Function<String, String> emailToName) {
|
|
StringBuilder buf = new StringBuilder();
|
|
buf.append('[');
|
|
for (RecipientType type : ImmutableList.of(TO, CC, BCC)) {
|
|
buf.append('\n');
|
|
buf.append(type);
|
|
buf.append(':');
|
|
String delim = " ";
|
|
for (String r : recipients.get(type)) {
|
|
buf.append(delim);
|
|
buf.append(emailToName.apply(r));
|
|
delim = ", ";
|
|
}
|
|
}
|
|
buf.append("\n]");
|
|
return buf.toString();
|
|
}
|
|
|
|
List<String> parseAddresses(Message msg, String headerName) {
|
|
EmailHeader header = msg.headers().get(headerName);
|
|
if (header == null) {
|
|
return ImmutableList.of();
|
|
}
|
|
Truth.assertThat(header).isInstanceOf(AddressList.class);
|
|
AddressList addrList = (AddressList) header;
|
|
return addrList.getAddressList().stream().map(Address::getEmail).collect(toList());
|
|
}
|
|
|
|
public FakeEmailSenderSubject to(String... emails) {
|
|
return rcpt(users.supportReviewersByEmail ? TO : null, emails);
|
|
}
|
|
|
|
public FakeEmailSenderSubject cc(String... emails) {
|
|
return rcpt(users.supportReviewersByEmail ? CC : null, emails);
|
|
}
|
|
|
|
public FakeEmailSenderSubject bcc(String... emails) {
|
|
return rcpt(users.supportReviewersByEmail ? BCC : null, emails);
|
|
}
|
|
|
|
private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, String[] emails) {
|
|
for (String email : emails) {
|
|
rcpt(type, email);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
private void rcpt(@Nullable RecipientType type, String email) {
|
|
rcpt(TO, email, TO.equals(type));
|
|
rcpt(CC, email, CC.equals(type));
|
|
rcpt(BCC, email, BCC.equals(type));
|
|
}
|
|
|
|
private void rcpt(@Nullable RecipientType type, String email, boolean expected) {
|
|
if (recipients.get(type).contains(email) != expected) {
|
|
failWithoutActual(
|
|
fact(
|
|
expected ? "expected to notify" : "expected not to notify",
|
|
type + ": " + users.emailToName(email)),
|
|
fact("but notified", recipientMapToString(recipients, users::emailToName)));
|
|
}
|
|
if (expected) {
|
|
accountedFor.add(email);
|
|
}
|
|
}
|
|
|
|
public FakeEmailSenderSubject noOneElse() {
|
|
for (Map.Entry<NotifyType, TestAccount> watchEntry : users.watchers.entrySet()) {
|
|
if (!accountedFor.contains(watchEntry.getValue().email())) {
|
|
notTo(watchEntry.getKey());
|
|
}
|
|
}
|
|
|
|
Map<RecipientType, List<String>> unaccountedFor = new HashMap<>();
|
|
boolean ok = true;
|
|
for (Map.Entry<RecipientType, List<String>> entry : recipients.entrySet()) {
|
|
unaccountedFor.put(entry.getKey(), new ArrayList<>());
|
|
for (String address : entry.getValue()) {
|
|
if (!accountedFor.contains(address)) {
|
|
unaccountedFor.get(entry.getKey()).add(address);
|
|
ok = false;
|
|
}
|
|
}
|
|
}
|
|
if (!ok) {
|
|
failWithoutActual(
|
|
fact(
|
|
"expected assertions for",
|
|
recipientMapToString(unaccountedFor, e -> users.emailToName(e))));
|
|
}
|
|
return this;
|
|
}
|
|
|
|
public FakeEmailSenderSubject notTo(String... emails) {
|
|
return rcpt(null, emails);
|
|
}
|
|
|
|
public FakeEmailSenderSubject to(TestAccount... accounts) {
|
|
return rcpt(TO, accounts);
|
|
}
|
|
|
|
public FakeEmailSenderSubject cc(TestAccount... accounts) {
|
|
return rcpt(CC, accounts);
|
|
}
|
|
|
|
public FakeEmailSenderSubject bcc(TestAccount... accounts) {
|
|
return rcpt(BCC, accounts);
|
|
}
|
|
|
|
public FakeEmailSenderSubject notTo(TestAccount... accounts) {
|
|
return rcpt(null, accounts);
|
|
}
|
|
|
|
private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, TestAccount[] accounts) {
|
|
for (TestAccount account : accounts) {
|
|
rcpt(type, account);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
private void rcpt(@Nullable RecipientType type, TestAccount account) {
|
|
rcpt(type, account.email());
|
|
}
|
|
|
|
public FakeEmailSenderSubject to(NotifyType... watches) {
|
|
return rcpt(TO, watches);
|
|
}
|
|
|
|
public FakeEmailSenderSubject cc(NotifyType... watches) {
|
|
return rcpt(CC, watches);
|
|
}
|
|
|
|
public FakeEmailSenderSubject bcc(NotifyType... watches) {
|
|
return rcpt(BCC, watches);
|
|
}
|
|
|
|
public FakeEmailSenderSubject notTo(NotifyType... watches) {
|
|
return rcpt(null, watches);
|
|
}
|
|
|
|
private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, NotifyType[] watches) {
|
|
for (NotifyType watch : watches) {
|
|
rcpt(type, watch);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
private void rcpt(@Nullable RecipientType type, NotifyType watch) {
|
|
if (!users.watchers.containsKey(watch)) {
|
|
failWithoutActual(fact("expected to be configured to watch", watch));
|
|
}
|
|
rcpt(type, users.watchers.get(watch));
|
|
}
|
|
}
|
|
|
|
private static final Map<String, StagedUsers> stagedUsers = new HashMap<>();
|
|
|
|
// TestAccount doesn't implement hashCode/equals, so this set is according
|
|
// to object identity. That's fine for our purposes.
|
|
private Set<TestAccount> accountsModifyingEmailStrategy = new HashSet<>();
|
|
|
|
@After
|
|
public void resetEmailStrategies() throws Exception {
|
|
for (TestAccount account : accountsModifyingEmailStrategy) {
|
|
setEmailStrategy(account, EmailStrategy.ENABLED, false);
|
|
}
|
|
accountsModifyingEmailStrategy.clear();
|
|
}
|
|
|
|
protected class StagedUsers {
|
|
public final TestAccount owner;
|
|
public final TestAccount author;
|
|
public final TestAccount uploader;
|
|
public final TestAccount reviewer;
|
|
public final TestAccount ccer;
|
|
public final TestAccount starrer;
|
|
public final TestAccount assignee;
|
|
public final TestAccount watchingProjectOwner;
|
|
public final String reviewerByEmail = "reviewerByEmail@example.com";
|
|
public final String ccerByEmail = "ccByEmail@example.com";
|
|
private final Map<NotifyType, TestAccount> watchers = new HashMap<>();
|
|
private final Map<String, TestAccount> accountsByEmail = new HashMap<>();
|
|
|
|
public boolean supportReviewersByEmail;
|
|
|
|
private String usersCacheKey() {
|
|
return description.getClassName();
|
|
}
|
|
|
|
private TestAccount evictAndCopy(TestAccount account) {
|
|
evictAndReindexAccount(account.id());
|
|
return account;
|
|
}
|
|
|
|
public StagedUsers() throws Exception {
|
|
synchronized (stagedUsers) {
|
|
if (stagedUsers.containsKey(usersCacheKey())) {
|
|
StagedUsers existing = stagedUsers.get(usersCacheKey());
|
|
owner = evictAndCopy(existing.owner);
|
|
author = evictAndCopy(existing.author);
|
|
uploader = evictAndCopy(existing.uploader);
|
|
reviewer = evictAndCopy(existing.reviewer);
|
|
ccer = evictAndCopy(existing.ccer);
|
|
starrer = evictAndCopy(existing.starrer);
|
|
assignee = evictAndCopy(existing.assignee);
|
|
watchingProjectOwner = evictAndCopy(existing.watchingProjectOwner);
|
|
watchers.putAll(existing.watchers);
|
|
return;
|
|
}
|
|
|
|
owner = testAccount("owner");
|
|
reviewer = testAccount("reviewer");
|
|
author = testAccount("author");
|
|
uploader = testAccount("uploader");
|
|
ccer = testAccount("ccer");
|
|
starrer = testAccount("starrer");
|
|
assignee = testAccount("assignee");
|
|
|
|
watchingProjectOwner = testAccount("watchingProjectOwner", "Administrators");
|
|
requestScopeOperations.setApiUser(watchingProjectOwner.id());
|
|
watch(allProjects.get(), pwi -> pwi.notifyNewChanges = true);
|
|
|
|
for (NotifyType watch : NotifyType.values()) {
|
|
if (watch == NotifyType.ALL) {
|
|
continue;
|
|
}
|
|
TestAccount watcher = testAccount(watch.toString());
|
|
requestScopeOperations.setApiUser(watcher.id());
|
|
watch(
|
|
allProjects.get(),
|
|
pwi -> {
|
|
pwi.notifyAllComments = watch.equals(NotifyType.ALL_COMMENTS);
|
|
pwi.notifyAbandonedChanges = watch.equals(NotifyType.ABANDONED_CHANGES);
|
|
pwi.notifyNewChanges = watch.equals(NotifyType.NEW_CHANGES);
|
|
pwi.notifyNewPatchSets = watch.equals(NotifyType.NEW_PATCHSETS);
|
|
pwi.notifySubmittedChanges = watch.equals(NotifyType.SUBMITTED_CHANGES);
|
|
});
|
|
watchers.put(watch, watcher);
|
|
}
|
|
|
|
stagedUsers.put(usersCacheKey(), this);
|
|
}
|
|
}
|
|
|
|
private String email(String username) {
|
|
// Email validator rejects usernames longer than 64 bytes.
|
|
if (username.length() > 64) {
|
|
username = username.substring(username.length() - 64);
|
|
if (username.startsWith(".")) {
|
|
username = username.substring(1);
|
|
}
|
|
}
|
|
return username + "@example.com";
|
|
}
|
|
|
|
public TestAccount testAccount(String name) throws Exception {
|
|
String username = name(name);
|
|
TestAccount account = accountCreator.create(username, email(username), name);
|
|
accountsByEmail.put(account.email(), account);
|
|
return account;
|
|
}
|
|
|
|
public TestAccount testAccount(String name, String groupName) throws Exception {
|
|
String username = name(name);
|
|
TestAccount account = accountCreator.create(username, email(username), name, groupName);
|
|
accountsByEmail.put(account.email(), account);
|
|
return account;
|
|
}
|
|
|
|
String emailToName(String email) {
|
|
if (accountsByEmail.containsKey(email)) {
|
|
return accountsByEmail.get(email).fullName();
|
|
}
|
|
return email;
|
|
}
|
|
|
|
protected void addReviewers(PushOneCommit.Result r) throws Exception {
|
|
ReviewInput in =
|
|
ReviewInput.noScore()
|
|
.reviewer(reviewer.email())
|
|
.reviewer(reviewerByEmail)
|
|
.reviewer(ccer.email(), ReviewerState.CC, false)
|
|
.reviewer(ccerByEmail, ReviewerState.CC, false);
|
|
ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
|
|
supportReviewersByEmail = true;
|
|
if (result.reviewers.values().stream().anyMatch(v -> v.error != null)) {
|
|
supportReviewersByEmail = false;
|
|
in =
|
|
ReviewInput.noScore()
|
|
.reviewer(reviewer.email())
|
|
.reviewer(ccer.email(), ReviewerState.CC, false);
|
|
result = gApi.changes().id(r.getChangeId()).current().review(in);
|
|
}
|
|
Truth.assertThat(result.reviewers.values().stream().allMatch(v -> v.error == null)).isTrue();
|
|
}
|
|
}
|
|
|
|
protected interface PushOptionGenerator {
|
|
List<String> pushOptions(StagedUsers users);
|
|
}
|
|
|
|
protected class StagedPreChange extends StagedUsers {
|
|
public final TestRepository<?> repo;
|
|
protected final PushOneCommit.Result result;
|
|
public final String changeId;
|
|
|
|
StagedPreChange(String ref) throws Exception {
|
|
this(ref, null);
|
|
}
|
|
|
|
StagedPreChange(String ref, @Nullable PushOptionGenerator pushOptionGenerator)
|
|
throws Exception {
|
|
super();
|
|
List<String> pushOptions = null;
|
|
if (pushOptionGenerator != null) {
|
|
pushOptions = pushOptionGenerator.pushOptions(this);
|
|
}
|
|
if (pushOptions != null) {
|
|
ref = ref + '%' + Joiner.on(',').join(pushOptions);
|
|
}
|
|
requestScopeOperations.setApiUser(owner.id());
|
|
repo = cloneProject(project, owner);
|
|
PushOneCommit push = pushFactory.create(owner.newIdent(), repo);
|
|
result = push.to(ref);
|
|
result.assertOkStatus();
|
|
changeId = result.getChangeId();
|
|
}
|
|
}
|
|
|
|
protected StagedPreChange stagePreChange(String ref) throws Exception {
|
|
return new StagedPreChange(ref);
|
|
}
|
|
|
|
protected StagedPreChange stagePreChange(
|
|
String ref, @Nullable PushOptionGenerator pushOptionGenerator) throws Exception {
|
|
return new StagedPreChange(ref, pushOptionGenerator);
|
|
}
|
|
|
|
protected class StagedChange extends StagedPreChange {
|
|
StagedChange(String ref) throws Exception {
|
|
super(ref);
|
|
|
|
requestScopeOperations.setApiUser(starrer.id());
|
|
gApi.accounts().self().starChange(result.getChangeId());
|
|
|
|
requestScopeOperations.setApiUser(owner.id());
|
|
addReviewers(result);
|
|
sender.clear();
|
|
}
|
|
}
|
|
|
|
protected StagedChange stageReviewableChange() throws Exception {
|
|
StagedChange sc = new StagedChange("refs/for/master");
|
|
sender.clear();
|
|
return sc;
|
|
}
|
|
|
|
protected StagedChange stageWipChange() throws Exception {
|
|
StagedChange sc = new StagedChange("refs/for/master%wip");
|
|
sender.clear();
|
|
return sc;
|
|
}
|
|
|
|
protected StagedChange stageReviewableWipChange() throws Exception {
|
|
StagedChange sc = stageReviewableChange();
|
|
requestScopeOperations.setApiUser(sc.owner.id());
|
|
gApi.changes().id(sc.changeId).setWorkInProgress();
|
|
sender.clear();
|
|
return sc;
|
|
}
|
|
|
|
protected StagedChange stageAbandonedReviewableChange() throws Exception {
|
|
StagedChange sc = stageReviewableChange();
|
|
requestScopeOperations.setApiUser(sc.owner.id());
|
|
gApi.changes().id(sc.changeId).abandon();
|
|
sender.clear();
|
|
return sc;
|
|
}
|
|
|
|
protected StagedChange stageAbandonedReviewableWipChange() throws Exception {
|
|
StagedChange sc = stageReviewableWipChange();
|
|
requestScopeOperations.setApiUser(sc.owner.id());
|
|
gApi.changes().id(sc.changeId).abandon();
|
|
sender.clear();
|
|
return sc;
|
|
}
|
|
|
|
protected StagedChange stageAbandonedWipChange() throws Exception {
|
|
StagedChange sc = stageWipChange();
|
|
requestScopeOperations.setApiUser(sc.owner.id());
|
|
gApi.changes().id(sc.changeId).abandon();
|
|
sender.clear();
|
|
return sc;
|
|
}
|
|
}
|