blob: bd156f4e423b87efe3c23ac4711f6f10e9aa4348 [file] [log] [blame]
// Copyright (C) 2013 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.api.change;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
import static com.google.gerrit.extensions.client.ChangeStatus.MERGED;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
import static com.google.gerrit.extensions.client.ListChangesOption.PUSH_CERTIFICATES;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
import static com.google.gerrit.extensions.client.ReviewerState.CC;
import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
import static com.google.gerrit.server.group.SystemGroupBackend.PROJECT_OWNERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.truth.CacheStatsSubject.assertThat;
import static com.google.gerrit.truth.CacheStatsSubject.cloneStats;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheStats;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.truth.ThrowableSubject;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ChangeIndexedCounter;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.UseTimezone;
import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
import com.google.gerrit.acceptance.api.change.ChangeIT.TestAttentionSetListenerModule.TestAttentionSetListener;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.server.change.CommentsUtil;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
import com.google.gerrit.extensions.api.changes.AttentionSetInput;
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
import com.google.gerrit.extensions.api.changes.DraftApi;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.NotifyInfo;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewResult;
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.api.changes.ReviewerResult;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.groups.GroupApi;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.Comment.Range;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
import com.google.gerrit.extensions.events.AttentionSetListener;
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.testing.TestChangeETagComputation;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.ChangeMessageModifier;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.index.change.IndexedChangeQuery;
import com.google.gerrit.server.patch.DiffSummary;
import com.google.gerrit.server.patch.DiffSummaryKey;
import com.google.gerrit.server.patch.IntraLineDiff;
import com.google.gerrit.server.patch.IntraLineDiffKey;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
import com.google.gerrit.server.restapi.change.PostReview;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.util.AccountTemplateUtil;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.Timestamp;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
import org.junit.Test;
@NoHttpd
@UseTimezone(timezone = "US/Eastern")
@VerifyNoPiiInChangeNotes(true)
public class ChangeIT extends AbstractDaemonTest {
@Inject private AccountOperations accountOperations;
@Inject private ChangeIndexCollection changeIndexCollection;
@Inject private GroupOperations groupOperations;
@Inject private IndexConfig indexConfig;
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
@Inject private IndexOperations.Change changeIndexOperations;
@Inject
@Named("diff_intraline")
private Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
@Inject
@Named("diff_summary")
private Cache<DiffSummaryKey, DiffSummary> diffSummaryCache;
@Test
public void get() throws Exception {
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
ChangeInfo c = info(triplet);
assertThat(c.id).isEqualTo(triplet);
assertThat(c.project).isEqualTo(project.get());
assertThat(c.branch).isEqualTo("master");
assertThat(c.status).isEqualTo(ChangeStatus.NEW);
assertThat(c.subject).isEqualTo("test commit");
assertThat(c.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
assertThat(c.mergeable).isNull();
assertThat(c.changeId).isEqualTo(r.getChangeId());
assertThat(c.created).isEqualTo(c.updated);
assertThat(c._number).isEqualTo(r.getChange().getId().get());
assertThat(c.owner._accountId).isEqualTo(admin.id().get());
assertThat(c.owner.name).isNull();
assertThat(c.owner.email).isNull();
assertThat(c.owner.username).isNull();
assertThat(c.owner.avatars).isNull();
assertThat(c.submissionId).isNull();
}
@Test
public void diffStatShouldComputeInsertionsAndDeletions() throws Exception {
String fileName = "a_new_file.txt";
String fileContent = "First line\nSecond line\n";
PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
String triplet = project.get() + "~master~" + result.getChangeId();
ChangeInfo change = gApi.changes().id(triplet).get();
assertThat(change.insertions).isNotNull();
assertThat(change.deletions).isNotNull();
}
@Test
public void diffStatShouldSkipInsertionsAndDeletions() throws Exception {
String fileName = "a_new_file.txt";
String fileContent = "First line\nSecond line\n";
PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
String triplet = project.get() + "~master~" + result.getChangeId();
ChangeInfo change =
gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
assertThat(change.insertions).isNull();
assertThat(change.deletions).isNull();
}
@Test
public void skipDiffstatOptionAvoidsAllDiffComputations() throws Exception {
String fileName = "a_new_file.txt";
String fileContent = "First line\nSecond line\n";
PushOneCommit.Result result = createChange("Add a file", fileName, fileContent);
String triplet = project.get() + "~master~" + result.getChangeId();
CacheStats startIntra = cloneStats(intraCache.stats());
CacheStats startSummary = cloneStats(diffSummaryCache.stats());
gApi.changes().id(triplet).get(ImmutableList.of(ListChangesOption.SKIP_DIFFSTAT));
assertThat(intraCache.stats()).since(startIntra).hasMissCount(0);
assertThat(intraCache.stats()).since(startIntra).hasHitCount(0);
assertThat(diffSummaryCache.stats()).since(startSummary).hasMissCount(0);
assertThat(diffSummaryCache.stats()).since(startSummary).hasHitCount(0);
}
@Test
@GerritConfig(name = "change.mergeabilityComputationBehavior", value = "NEVER")
public void excludeMergeableInChangeInfo() throws Exception {
PushOneCommit.Result r = createChange();
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
assertThat(c.mergeable).isNull();
}
@Test
public void getSubmissionId() throws Exception {
PushOneCommit.Result changeResult = createChange();
String changeId = changeResult.getChangeId();
merge(changeResult);
assertThat(gApi.changes().id(changeId).get().submissionId).isNotNull();
}
@Test
public void setWorkInProgressNotAllowedWithoutPermission() throws Exception {
PushOneCommit.Result rwip = createChange();
String changeId = rwip.getChangeId();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.changes().id(changeId).setWorkInProgress());
assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
}
@Test
public void setWorkInProgressAllowedAsAdmin() throws Exception {
requestScopeOperations.setApiUser(user.id());
String changeId =
gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(changeId).setWorkInProgress();
assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
}
@Test
public void setWorkInProgressAllowedAsProjectOwner() throws Exception {
requestScopeOperations.setApiUser(user.id());
String changeId =
gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user2.id());
gApi.changes().id(changeId).setWorkInProgress();
assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
}
@Test
public void createWipChangeWithWorkInProgressByDefaultForProject() throws Exception {
ConfigInput input = new ConfigInput();
input.workInProgressByDefault = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(input);
String changeId =
gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
}
@Test
public void setReadyForReviewNotAllowedWithoutPermission() throws Exception {
PushOneCommit.Result rready = createChange();
String changeId = rready.getChangeId();
gApi.changes().id(changeId).setWorkInProgress();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.changes().id(changeId).setReadyForReview());
assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
}
@Test
public void setReadyForReviewAllowedAsAdmin() throws Exception {
requestScopeOperations.setApiUser(user.id());
String changeId =
gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
gApi.changes().id(changeId).setWorkInProgress();
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(changeId).setReadyForReview();
assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
}
@Test
public void setReadyForReviewAllowedAsProjectOwner() throws Exception {
requestScopeOperations.setApiUser(user.id());
String changeId =
gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
gApi.changes().id(changeId).setWorkInProgress();
com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user2.id());
gApi.changes().id(changeId).setReadyForReview();
assertThat(gApi.changes().id(changeId).get().workInProgress).isNull();
}
@Test
public void hasReviewStarted() throws Exception {
PushOneCommit.Result r = createWorkInProgressChange();
String changeId = r.getChangeId();
ChangeInfo info = gApi.changes().id(changeId).get();
assertThat(info.hasReviewStarted).isFalse();
gApi.changes().id(changeId).setReadyForReview();
info = gApi.changes().id(changeId).get();
assertThat(info.hasReviewStarted).isTrue();
}
@Test
public void pendingReviewers() throws Exception {
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
PushOneCommit.Result r = createWorkInProgressChange();
String changeId = r.getChangeId();
assertThat(gApi.changes().id(changeId).get().pendingReviewers).isEmpty();
// Add some pending reviewers.
String email1 = name("user1") + "@example.com";
String email2 = name("user2") + "@example.com";
String email3 = name("user3") + "@example.com";
String email4 = name("user4") + "@example.com";
accountOperations
.newAccount()
.username(name("user1"))
.preferredEmail(email1)
.fullname("User1")
.create();
accountOperations
.newAccount()
.username(name("user2"))
.preferredEmail(email2)
.fullname("User2")
.create();
accountOperations
.newAccount()
.username(name("user3"))
.preferredEmail(email3)
.fullname("User3")
.create();
accountOperations
.newAccount()
.username(name("user4"))
.preferredEmail(email4)
.fullname("User4")
.create();
ReviewInput in =
ReviewInput.noScore()
.reviewer(email1)
.reviewer(email2)
.reviewer(email3, CC, false)
.reviewer(email4, CC, false)
.reviewer("byemail1@example.com")
.reviewer("byemail2@example.com")
.reviewer("byemail3@example.com", CC, false)
.reviewer("byemail4@example.com", CC, false);
ReviewResult result = gApi.changes().id(changeId).current().review(in);
assertThat(result.reviewers).isNotEmpty();
ChangeInfo info = gApi.changes().id(changeId).get();
Function<Collection<AccountInfo>, Collection<String>> toEmails =
ais -> ais.stream().map(ai -> ai.email).collect(toSet());
assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
.containsExactly(email1, email2, "byemail1@example.com", "byemail2@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
.containsExactly(email3, email4, "byemail3@example.com", "byemail4@example.com");
assertThat(info.pendingReviewers.get(REMOVED)).isNull();
// Stage some pending reviewer removals.
gApi.changes().id(changeId).reviewer(email1).remove();
gApi.changes().id(changeId).reviewer(email3).remove();
gApi.changes().id(changeId).reviewer("byemail1@example.com").remove();
gApi.changes().id(changeId).reviewer("byemail3@example.com").remove();
info = gApi.changes().id(changeId).get();
assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
.containsExactly(email2, "byemail2@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
.containsExactly(email4, "byemail4@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
.containsExactly(email1, email3, "byemail1@example.com", "byemail3@example.com");
// "Undo" a removal.
in = ReviewInput.noScore().reviewer(email1);
gApi.changes().id(changeId).current().review(in);
info = gApi.changes().id(changeId).get();
assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
.containsExactly(email1, email2, "byemail2@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
.containsExactly(email4, "byemail4@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
.containsExactly(email3, "byemail1@example.com", "byemail3@example.com");
// "Commit" by moving out of WIP.
gApi.changes().id(changeId).setReadyForReview();
info = gApi.changes().id(changeId).get();
assertThat(info.pendingReviewers).isEmpty();
assertThat(toEmails.apply(info.reviewers.get(REVIEWER)))
.containsExactly(email1, email2, "byemail2@example.com");
assertThat(toEmails.apply(info.reviewers.get(CC)))
.containsExactly(email4, "byemail4@example.com");
assertThat(info.reviewers.get(REMOVED)).isNull();
}
@Test
public void toggleWorkInProgressState() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
// With message
gApi.changes().id(changeId).setWorkInProgress("Needs some refactoring");
ChangeInfo info = gApi.changes().id(changeId).get();
assertThat(info.workInProgress).isTrue();
assertThat(Iterables.getLast(info.messages).message).contains("Needs some refactoring");
assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP);
gApi.changes().id(changeId).setReadyForReview("PTAL");
info = gApi.changes().id(changeId).get();
assertThat(info.workInProgress).isNull();
assertThat(Iterables.getLast(info.messages).message).contains("PTAL");
assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
// No message
gApi.changes().id(changeId).setWorkInProgress();
info = gApi.changes().id(changeId).get();
assertThat(info.workInProgress).isTrue();
assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set Work In Progress");
assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP);
gApi.changes().id(changeId).setReadyForReview();
info = gApi.changes().id(changeId).get();
assertThat(info.workInProgress).isNull();
assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set Ready For Review");
assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
}
@Test
public void toggleWorkInProgressStateByNonOwnerWithPermission() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String refactor = "Needs some refactoring";
String ptal = "PTAL";
projectOperations
.project(project)
.forUpdate()
.add(
allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
.ref("refs/heads/master")
.group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(changeId).setWorkInProgress(refactor);
ChangeInfo info = gApi.changes().id(changeId).get();
assertThat(info.workInProgress).isTrue();
assertThat(Iterables.getLast(info.messages).message).contains(refactor);
assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP);
gApi.changes().id(changeId).setReadyForReview(ptal);
info = gApi.changes().id(changeId).get();
assertThat(info.workInProgress).isNull();
assertThat(Iterables.getLast(info.messages).message).contains(ptal);
assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY);
}
@Test
public void reviewAndStartReview() throws Exception {
PushOneCommit.Result r = createWorkInProgressChange();
r.assertOkStatus();
assertThat(r.getChange().change().isWorkInProgress()).isTrue();
ReviewInput in = ReviewInput.noScore().setWorkInProgress(false);
ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
assertThat(result.ready).isTrue();
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.workInProgress).isNull();
}
@Test
public void reviewAndMoveToWorkInProgress() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(r.getChange().change().isWorkInProgress()).isFalse();
ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
assertThat(result.ready).isNull();
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.workInProgress).isTrue();
}
@Test
public void reviewAndSetWorkInProgressAndAddReviewerAndVote() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(r.getChange().change().isWorkInProgress()).isFalse();
ReviewInput in =
ReviewInput.approve()
.reviewer(user.email())
.label(LabelId.CODE_REVIEW, 1)
.setWorkInProgress(true);
gApi.changes().id(r.getChangeId()).current().review(in);
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.workInProgress).isTrue();
assertThat(info.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
.containsExactly(admin.id().get(), user.id().get());
assertThat(info.labels.get(LabelId.CODE_REVIEW).recommended._accountId)
.isEqualTo(admin.id().get());
}
@Test
public void reviewWithWorkInProgressAndReadyReturnsError() throws Exception {
PushOneCommit.Result r = createChange();
ReviewInput in = ReviewInput.noScore();
in.ready = true;
in.workInProgress = true;
ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
assertThat(result.error).isEqualTo(PostReview.ERROR_WIP_READY_MUTUALLY_EXCLUSIVE);
}
@Test
@TestProjectInput(cloneAs = "user1")
public void reviewWithWorkInProgressChangeOwner() throws Exception {
PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
requestScopeOperations.setApiUser(user.id());
ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
gApi.changes().id(r.getChangeId()).current().review(in);
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.workInProgress).isTrue();
}
@Test
@TestProjectInput(cloneAs = "user1")
public void reviewWithWithWorkInProgressAdmin() throws Exception {
PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
assertThat(r.getChange().change().getOwner()).isEqualTo(user.id());
requestScopeOperations.setApiUser(admin.id());
ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
gApi.changes().id(r.getChangeId()).current().review(in);
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.workInProgress).isTrue();
}
@Test
public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception {
PushOneCommit.Result r = createChange();
ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in));
assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
}
@Test
public void reviewWithWorkInProgressByNonOwnerWithPermission() throws Exception {
PushOneCommit.Result r = createChange();
ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
projectOperations
.project(project)
.forUpdate()
.add(
allow(Permission.TOGGLE_WORK_IN_PROGRESS_STATE)
.ref("refs/heads/master")
.group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(r.getChangeId()).current().review(in);
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.workInProgress).isTrue();
}
@Test
public void reviewWithReadyByNonOwnerReturnsError() throws Exception {
PushOneCommit.Result r = createChange();
change(r).setWorkInProgress();
ReviewInput in = ReviewInput.noScore().setReady(true);
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in));
assertThat(thrown).hasMessageThat().contains("toggle work in progress state not permitted");
}
@Test
public void getAmbiguous() throws Exception {
PushOneCommit.Result r1 = createChange();
String changeId = r1.getChangeId();
gApi.changes().id(changeId).get();
BranchInput b = new BranchInput();
b.revision = repo().exactRef("HEAD").getObjectId().name();
gApi.projects().name(project.get()).branch("other").create(b);
PushOneCommit push2 =
pushFactory.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT,
PushOneCommit.FILE_NAME,
PushOneCommit.FILE_CONTENT,
changeId);
PushOneCommit.Result r2 = push2.to("refs/for/other");
assertThat(r2.getChangeId()).isEqualTo(changeId);
ResourceNotFoundException thrown =
assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(changeId).get());
assertThat(thrown).hasMessageThat().contains("Multiple changes found for " + changeId);
}
@FunctionalInterface
private interface Rebase {
void call(String id) throws RestApiException;
}
@Test
public void rebaseViaRevisionApi() throws Exception {
testRebase(id -> gApi.changes().id(id).current().rebase());
}
@Test
public void rebaseViaChangeApi() throws Exception {
testRebase(id -> gApi.changes().id(id).rebase());
}
private void testRebase(Rebase rebase) throws Exception {
// Create two changes both with the same parent
PushOneCommit.Result r = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
// Approve and submit the first change
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
revision.review(ReviewInput.approve());
revision.submit();
// Add an approval whose score should be copied on trivial rebase
gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
String changeId = r2.getChangeId();
// Rebase the second change
rebase.call(changeId);
// Second change should have 2 patch sets and an approval
ChangeInfo c2 = gApi.changes().id(changeId).get(CURRENT_REVISION, DETAILED_LABELS);
assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2);
// ...and the committer and description should be correct
ChangeInfo info = gApi.changes().id(changeId).get(CURRENT_REVISION, CURRENT_COMMIT);
GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
assertThat(committer.name).isEqualTo(admin.fullName());
assertThat(committer.email).isEqualTo(admin.email());
String description = info.revisions.get(info.currentRevision).description;
assertThat(description).isEqualTo("Rebase");
// ...and the approval was copied
LabelInfo cr = c2.labels.get(LabelId.CODE_REVIEW);
assertThat(cr).isNotNull();
assertThat(cr.all).hasSize(1);
assertThat(cr.all.get(0).value).isEqualTo(1);
// Rebasing the second change again should fail
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(changeId).current().rebase());
assertThat(thrown).hasMessageThat().contains("Change is already up to date");
}
@Test
public void rebaseAsUploaderInAttentionSet() throws Exception {
// Create two changes both with the same parent
PushOneCommit.Result r = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
// Approve and submit the first change
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
revision.review(ReviewInput.approve());
revision.submit();
TestAccount admin2 = accountCreator.admin2();
requestScopeOperations.setApiUser(admin2.id());
amendChangeWithUploader(r2, project, admin2);
gApi.changes()
.id(r2.getChangeId())
.addToAttentionSet(new AttentionSetInput(admin2.id().toString(), "manual update"));
gApi.changes().id(r2.getChangeId()).rebase();
}
@Test
public void rebaseOnChangeNumber() throws Exception {
String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
PushOneCommit.Result r1 = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
Change.Id id1 = r1.getChange().getId();
RebaseInput in = new RebaseInput();
in.base = id1.toString();
gApi.changes().id(r2.getChangeId()).rebase(in);
Change.Id id2 = r2.getChange().getId();
ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
ri2 = ci2.revisions.get(ci2.currentRevision);
assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
List<RelatedChangeAndCommitInfo> related =
gApi.changes().id(id2.get()).revision(ri2._number).related().changes;
assertThat(related).hasSize(2);
assertThat(related.get(0)._changeNumber).isEqualTo(id2.get());
assertThat(related.get(0)._revisionNumber).isEqualTo(2);
assertThat(related.get(1)._changeNumber).isEqualTo(id1.get());
assertThat(related.get(1)._revisionNumber).isEqualTo(1);
}
@Test
public void rebaseOnClosedChange() throws Exception {
String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
PushOneCommit.Result r1 = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
// Submit first change.
Change.Id id1 = r1.getChange().getId();
gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
gApi.changes().id(id1.get()).current().submit();
// Rebase second change on first change.
RebaseInput in = new RebaseInput();
in.base = id1.toString();
gApi.changes().id(r2.getChangeId()).rebase(in);
Change.Id id2 = r2.getChange().getId();
ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
ri2 = ci2.revisions.get(ci2.currentRevision);
assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
assertThat(gApi.changes().id(id2.get()).revision(ri2._number).related().changes).isEmpty();
}
@Test
public void rebaseOnNonExistingChange() throws Exception {
String changeId = createChange().getChangeId();
RebaseInput in = new RebaseInput();
in.base = "999999";
UnprocessableEntityException exception =
assertThrows(
UnprocessableEntityException.class, () -> gApi.changes().id(changeId).rebase(in));
assertThat(exception).hasMessageThat().isEqualTo("Base change not found: " + in.base);
}
@Test
public void rebaseFromRelationChainToClosedChange() throws Exception {
PushOneCommit.Result r1 = createChange();
testRepo.reset("HEAD~1");
createChange();
PushOneCommit.Result r3 = createChange();
// Submit first change.
Change.Id id1 = r1.getChange().getId();
gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
gApi.changes().id(id1.get()).current().submit();
// Rebase third change on first change.
RebaseInput in = new RebaseInput();
in.base = id1.toString();
gApi.changes().id(r3.getChangeId()).rebase(in);
Change.Id id3 = r3.getChange().getId();
ChangeInfo ci3 = get(r3.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
RevisionInfo ri3 = ci3.revisions.get(ci3.currentRevision);
assertThat(ri3.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
assertThat(gApi.changes().id(id3.get()).revision(ri3._number).related().changes).isEmpty();
}
@Test
public void rebaseNotAllowedWithoutPermission() throws Exception {
// Create two changes both with the same parent
PushOneCommit.Result r = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
// Approve and submit the first change
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
revision.review(ReviewInput.approve());
revision.submit();
// Rebase the second
String changeId = r2.getChangeId();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
assertThat(thrown).hasMessageThat().contains("rebase not permitted");
}
@Test
public void rebaseAllowedWithPermission() throws Exception {
// Create two changes both with the same parent
PushOneCommit.Result r = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
// Approve and submit the first change
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
revision.review(ReviewInput.approve());
revision.submit();
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
.update();
// Rebase the second
String changeId = r2.getChangeId();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(changeId).rebase();
}
@Test
public void rebaseNotAllowedWithoutPushPermission() throws Exception {
// Create two changes both with the same parent
PushOneCommit.Result r = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
// Approve and submit the first change
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
revision.review(ReviewInput.approve());
revision.submit();
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
.add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
.update();
// Rebase the second
String changeId = r2.getChangeId();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
assertThat(thrown).hasMessageThat().contains("rebase not permitted");
}
@Test
public void rebaseNotAllowedForOwnerWithoutPushPermission() throws Exception {
// Create two changes both with the same parent
PushOneCommit.Result r = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
// Approve and submit the first change
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
revision.review(ReviewInput.approve());
revision.submit();
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
.update();
// Rebase the second
String changeId = r2.getChangeId();
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
assertThat(thrown).hasMessageThat().contains("rebase not permitted");
}
@Test
public void rebaseWithValidationOptions() throws Exception {
// Create two changes both with the same parent
PushOneCommit.Result r = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
// Approve and submit the first change
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
revision.review(ReviewInput.approve());
revision.submit();
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.validationOptions = ImmutableMap.of("key", "value");
TestCommitValidationListener testCommitValidationListener = new TestCommitValidationListener();
try (Registration registration =
extensionRegistry.newRegistration().add(testCommitValidationListener)) {
// Rebase the second change
gApi.changes().id(r2.getChangeId()).current().rebase(rebaseInput);
assertThat(testCommitValidationListener.receiveEvent.pushOptions)
.containsExactly("key", "value");
}
}
@Test
public void deleteNewChangeAsAdmin() throws Exception {
deleteChangeAsUser(admin, admin);
}
@Test
@TestProjectInput(cloneAs = "user1")
public void deleteNewChangeAsNormalUser() throws Exception {
PushOneCommit.Result changeResult =
pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
String changeId = changeResult.getChangeId();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
assertThat(thrown).hasMessageThat().contains("delete not permitted");
}
@Test
public void deleteNewChangeAsUserWithDeleteChangesPermissionForGroup() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.DELETE_CHANGES).ref("refs/*").group(REGISTERED_USERS))
.update();
deleteChangeAsUser(admin, user);
}
@Test
public void deleteNewChangeAsUserWithDeleteChangesPermissionForProjectOwners() throws Exception {
GroupApi groupApi = gApi.groups().create(name("delete-change"));
groupApi.addMembers("user1");
Project.NameKey nameKey = Project.nameKey(name("delete-change"));
ProjectInput in = new ProjectInput();
in.name = nameKey.get();
in.owners = Lists.newArrayListWithCapacity(1);
in.owners.add(groupApi.name());
in.createEmptyCommit = true;
gApi.projects().create(in);
projectOperations
.project(nameKey)
.forUpdate()
.add(allow(Permission.DELETE_CHANGES).ref("refs/*").group(PROJECT_OWNERS))
.update();
deleteChangeAsUser(nameKey, admin, user);
}
@Test
public void deleteChangeAsUserWithDeleteOwnChangesPermissionForGroup() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
.update();
deleteChangeAsUser(user, user);
}
@Test
public void deleteChangeAsUserWithDeleteOwnChangesPermissionForOwners() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(CHANGE_OWNER))
.update();
deleteChangeAsUser(user, user);
}
private void deleteChangeAsUser(
com.google.gerrit.acceptance.TestAccount owner,
com.google.gerrit.acceptance.TestAccount deleteAs)
throws Exception {
deleteChangeAsUser(project, owner, deleteAs);
}
private void deleteChangeAsUser(
Project.NameKey projectName,
com.google.gerrit.acceptance.TestAccount owner,
com.google.gerrit.acceptance.TestAccount deleteAs)
throws Exception {
try {
projectOperations
.project(projectName)
.forUpdate()
.add(allow(Permission.VIEW_PRIVATE_CHANGES).ref("refs/*").group(ANONYMOUS_USERS))
.update();
requestScopeOperations.setApiUser(owner.id());
ChangeInput in = new ChangeInput();
in.project = projectName.get();
in.branch = "refs/heads/master";
in.subject = "test";
ChangeInfo changeInfo = gApi.changes().create(in).get();
String changeId = changeInfo.changeId;
int id = changeInfo._number;
String commit = changeInfo.currentRevision;
assertThat(gApi.changes().id(changeId).info().owner._accountId).isEqualTo(owner.id().get());
requestScopeOperations.setApiUser(deleteAs.id());
gApi.changes().id(changeId).delete();
assertThat(query(changeId)).isEmpty();
String ref = Change.id(id).toRefPrefix() + "1";
eventRecorder.assertRefUpdatedEvents(projectName.get(), ref, null, commit, commit, null);
eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email());
} finally {
projectOperations
.project(project)
.forUpdate()
.remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*"))
.remove(permissionKey(Permission.DELETE_CHANGES).ref("refs/*"))
.update();
}
}
@Test
public void deleteNewChangeOfAnotherUserAsAdmin() throws Exception {
deleteChangeAsUser(user, admin);
}
@Test
public void deleteNewChangeOfAnotherUserWithDeleteOwnChangesPermission() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
.update();
try {
PushOneCommit.Result changeResult = createChange();
String changeId = changeResult.getChangeId();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
assertThat(thrown).hasMessageThat().contains("delete not permitted");
} finally {
projectOperations
.project(project)
.forUpdate()
.remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*"))
.update();
}
}
@Test
@TestProjectInput(createEmptyCommit = false)
public void deleteNewChangeForBranchWithoutCommits() throws Exception {
PushOneCommit.Result changeResult = createChange();
String changeId = changeResult.getChangeId();
gApi.changes().id(changeId).delete();
assertThat(query(changeId)).isEmpty();
}
@Test
@TestProjectInput(cloneAs = "user1")
public void deleteAbandonedChangeAsNormalUser() throws Exception {
PushOneCommit.Result changeResult =
pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
String changeId = changeResult.getChangeId();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(changeId).abandon();
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.changes().id(changeId).delete());
assertThat(thrown).hasMessageThat().contains("delete not permitted");
}
@Test
@TestProjectInput(cloneAs = "user1")
public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception {
PushOneCommit.Result changeResult =
pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
String changeId = changeResult.getChangeId();
gApi.changes().id(changeId).abandon();
gApi.changes().id(changeId).delete();
assertThat(query(changeId)).isEmpty();
}
@Test
public void deleteMergedChange() throws Exception {
PushOneCommit.Result changeResult = createChange();
String changeId = changeResult.getChangeId();
merge(changeResult);
MethodNotAllowedException thrown =
assertThrows(MethodNotAllowedException.class, () -> gApi.changes().id(changeId).delete());
assertThat(thrown).hasMessageThat().contains("delete not permitted");
}
@Test
@TestProjectInput(cloneAs = "user1")
public void deleteMergedChangeWithDeleteOwnChangesPermission() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.DELETE_OWN_CHANGES).ref("refs/*").group(REGISTERED_USERS))
.update();
try {
PushOneCommit.Result changeResult =
pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
String changeId = changeResult.getChangeId();
merge(changeResult);
requestScopeOperations.setApiUser(user.id());
MethodNotAllowedException thrown =
assertThrows(MethodNotAllowedException.class, () -> gApi.changes().id(changeId).delete());
assertThat(thrown).hasMessageThat().contains("delete not permitted");
} finally {
projectOperations
.project(project)
.forUpdate()
.remove(permissionKey(Permission.DELETE_OWN_CHANGES).ref("refs/*"))
.update();
}
}
@Test
public void deleteNewChangeWithMergedPatchSet() throws Exception {
PushOneCommit.Result changeResult = createChange();
String changeId = changeResult.getChangeId();
Change.Id id = changeResult.getChange().getId();
merge(changeResult);
setChangeStatus(id, Change.Status.NEW);
ResourceConflictException thrown =
assertThrows(ResourceConflictException.class, () -> gApi.changes().id(changeId).delete());
assertThat(thrown)
.hasMessageThat()
.contains(String.format("Cannot delete change %s: patch set 1 is already merged", id));
}
@Test
public void deleteChangeUpdatesIndex() throws Exception {
PushOneCommit.Result changeResult = createChange();
String changeId = changeResult.getChangeId();
Change.Id id = changeResult.getChange().getId();
ChangeIndex idx = changeIndexCollection.getSearchIndex();
Optional<ChangeData> result =
idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
assertThat(result).isPresent();
gApi.changes().id(changeId).delete();
result = idx.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, ImmutableSet.of()));
assertThat(result).isEmpty();
}
@Test
public void deleteChangeRemovesDraftComment() throws Exception {
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
DraftInput dri = new DraftInput();
dri.message = "hello";
dri.path = "a.txt";
dri.line = 1;
gApi.changes().id(r.getChangeId()).current().createDraft(dri);
Change.Id num = r.getChange().getId();
try (Repository repo = repoManager.openRepository(allUsers)) {
assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id())))
.isNotEmpty();
}
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(r.getChangeId()).delete();
try (Repository repo = repoManager.openRepository(allUsers)) {
assertThat(repo.getRefDatabase().getRefsByPrefix(RefNames.refsDraftComments(num, user.id())))
.isEmpty();
}
}
@Test
public void deleteChangeRemovesItsChangeEdit() throws Exception {
PushOneCommit.Result result = createChange();
requestScopeOperations.setApiUser(user.id());
String changeId = result.getChangeId();
gApi.changes().id(changeId).edit().create();
gApi.changes()
.id(changeId)
.edit()
.modifyFile(FILE_NAME, RawInputUtil.create("foo".getBytes(UTF_8)));
requestScopeOperations.setApiUser(admin.id());
try (Repository repo = repoManager.openRepository(project)) {
String expected =
RefNames.refsUsers(user.id()) + "/edit-" + result.getChange().getId() + "/1";
assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isNotEmpty();
gApi.changes().id(changeId).delete();
assertThat(repo.getRefDatabase().getRefsByPrefix(expected)).isEmpty();
}
}
@Test
public void deleteChangeDoesntRemoveOtherChangeEdits() throws Exception {
PushOneCommit.Result result = createChange();
PushOneCommit.Result irrelevantChangeResult = createChange();
requestScopeOperations.setApiUser(admin.id());
String changeId = result.getChangeId();
String irrelevantChangeId = irrelevantChangeResult.getChangeId();
gApi.changes().id(irrelevantChangeId).edit().create();
gApi.changes()
.id(irrelevantChangeId)
.edit()
.modifyFile(FILE_NAME, RawInputUtil.create("foo".getBytes(UTF_8)));
gApi.changes().id(changeId).delete();
assertThat(gApi.changes().id(irrelevantChangeId).edit().get()).isPresent();
}
@Test
public void rebaseUpToDateChange() throws Exception {
PushOneCommit.Result r = createChange();
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).rebase());
assertThat(thrown).hasMessageThat().contains("Change is already up to date");
}
@Test
public void rebaseConflict() throws Exception {
PushOneCommit.Result r1 = createChange();
gApi.changes()
.id(r1.getChangeId())
.revision(r1.getCommit().name())
.review(ReviewInput.approve());
gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT,
PushOneCommit.FILE_NAME,
"other content",
"If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5");
PushOneCommit.Result r2 = push.to("refs/for/master");
r2.assertOkStatus();
ResourceConflictException exception =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase());
assertThat(exception)
.hasMessageThat()
.isEqualTo(
String.format(
"The change could not be rebased due to a conflict during merge.\n\n"
+ "merge conflict(s):\n%s",
PushOneCommit.FILE_NAME));
}
@Test
public void rebaseDoesNotAddWorkInProgress() throws Exception {
PushOneCommit.Result r = createChange();
// create an unrelated change so that we can rebase
testRepo.reset("HEAD~1");
PushOneCommit.Result unrelated = createChange();
gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(unrelated.getChangeId()).current().submit();
gApi.changes().id(r.getChangeId()).rebase();
// change is still ready for review after rebase
assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isNull();
}
@Test
public void rebaseDoesNotRemoveWorkInProgress() throws Exception {
PushOneCommit.Result r = createChange();
change(r).setWorkInProgress();
// create an unrelated change so that we can rebase
testRepo.reset("HEAD~1");
PushOneCommit.Result unrelated = createChange();
gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(unrelated.getChangeId()).current().submit();
gApi.changes().id(r.getChangeId()).rebase();
// change is still work in progress after rebase
assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isTrue();
}
@Test
public void rebaseConflict_conflictsAllowed() throws Exception {
String patchSetSubject = "patch set change";
String patchSetContent = "patch set content";
String baseSubject = "base change";
String baseContent = "base content";
PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
gApi.changes()
.id(r1.getChangeId())
.revision(r1.getCommit().name())
.review(ReviewInput.approve());
gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
testRepo.reset("HEAD~1");
PushOneCommit push =
pushFactory.create(
admin.newIdent(), testRepo, patchSetSubject, PushOneCommit.FILE_NAME, patchSetContent);
PushOneCommit.Result r2 = push.to("refs/for/master");
r2.assertOkStatus();
String changeId = r2.getChangeId();
RevCommit patchSet = r2.getCommit();
RevCommit base = r1.getCommit();
TestWorkInProgressStateChangedListener wipStateChangedListener =
new TestWorkInProgressStateChangedListener();
try (Registration registration =
extensionRegistry.newRegistration().add(wipStateChangedListener)) {
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.allowConflicts = true;
ChangeInfo changeInfo =
gApi.changes().id(changeId).revision(patchSet.name()).rebaseAsInfo(rebaseInput);
assertThat(changeInfo.containsGitConflicts).isTrue();
assertThat(changeInfo.workInProgress).isTrue();
}
assertThat(wipStateChangedListener.invoked).isTrue();
assertThat(wipStateChangedListener.wip).isTrue();
// To get the revisions, we must retrieve the change with more change options.
ChangeInfo changeInfo =
gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
assertThat(changeInfo.revisions).hasSize(2);
assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
.isEqualTo(base.name());
// Verify that the file content in the created patch set is correct.
// We expect that it has conflict markers to indicate the conflict.
BinaryResult bin =
gApi.changes().id(changeId).current().file(PushOneCommit.FILE_NAME).content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String fileContent = new String(os.toByteArray(), UTF_8);
String patchSetSha1 = abbreviateName(patchSet, 6);
String baseSha1 = abbreviateName(base, 6);
assertThat(fileContent)
.isEqualTo(
"<<<<<<< PATCH SET ("
+ patchSetSha1
+ " "
+ patchSetSubject
+ ")\n"
+ patchSetContent
+ "\n"
+ "=======\n"
+ baseContent
+ "\n"
+ ">>>>>>> BASE ("
+ baseSha1
+ " "
+ baseSubject
+ ")\n");
// Verify the message that has been posted on the change.
List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
assertThat(messages).hasSize(2);
assertThat(Iterables.getLast(messages).message)
.isEqualTo(
"Patch Set 2: Patch Set 1 was rebased\n\n"
+ "The following files contain Git conflicts:\n"
+ "* "
+ PushOneCommit.FILE_NAME
+ "\n");
}
@Test
public void attentionSetListener_firesOnChange() throws Exception {
PushOneCommit.Result r1 = createChange();
AttentionSetInput addUser = new AttentionSetInput(user.email(), "some reason");
TestAttentionSetListener attentionSetListener = new TestAttentionSetListener();
try (Registration registration =
extensionRegistry.newRegistration().add(attentionSetListener)) {
gApi.changes().id(r1.getChangeId()).addReviewer(user.email());
assertThat(attentionSetListener.firedCount).isEqualTo(1);
assertThat(attentionSetListener.lastEvent.usersAdded().size()).isEqualTo(1);
attentionSetListener
.lastEvent
.usersAdded()
.forEach(u -> assertThat(u).isEqualTo(user.id().get()));
gApi.changes().id(r1.getChangeId()).addToAttentionSet(addUser);
assertThat(attentionSetListener.firedCount).isEqualTo(1);
gApi.changes().id(r1.getChangeId()).attention(user.username()).remove(addUser);
assertThat(attentionSetListener.firedCount).isEqualTo(2);
assertThat(attentionSetListener.lastEvent.usersAdded()).isEmpty();
assertThat(attentionSetListener.lastEvent.usersRemoved().size()).isEqualTo(1);
attentionSetListener
.lastEvent
.usersRemoved()
.forEach(u -> assertThat(u).isEqualTo(user.id().get()));
}
}
@Test
public void rebaseChangeBase() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
PushOneCommit.Result r3 = createChange();
RebaseInput ri = new RebaseInput();
// rebase r3 directly onto master (break dep. towards r2)
ri.base = "";
gApi.changes().id(r3.getChangeId()).revision(r3.getCommit().name()).rebase(ri);
PatchSet ps3 = r3.getPatchSet();
assertThat(ps3.id().get()).isEqualTo(2);
// rebase r2 onto r3 (referenced by ref)
ri.base = ps3.id().toRefName();
gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri);
PatchSet ps2 = r2.getPatchSet();
assertThat(ps2.id().get()).isEqualTo(2);
// rebase r1 onto r2 (referenced by commit)
ri.base = ps2.commitId().name();
gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri);
PatchSet ps1 = r1.getPatchSet();
assertThat(ps1.id().get()).isEqualTo(2);
// rebase r1 onto r3 (referenced by change number)
ri.base = String.valueOf(r3.getChange().getId().get());
gApi.changes().id(r1.getChangeId()).revision(ps1.commitId().name()).rebase(ri);
assertThat(r1.getPatchSetId().get()).isEqualTo(3);
}
@Test
public void rebaseChangeBaseRecursion() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
RebaseInput ri = new RebaseInput();
ri.base = r2.getCommit().name();
String expectedMessage =
"base change "
+ r2.getChangeId()
+ " is a descendant of the current change - recursion not allowed";
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri));
assertThat(thrown).hasMessageThat().contains(expectedMessage);
}
@Test
public void rebaseAbandonedChange() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
gApi.changes().id(changeId).abandon();
ChangeInfo info = info(changeId);
assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(changeId).revision(r.getCommit().name()).rebase());
assertThat(thrown).hasMessageThat().contains("change is abandoned");
}
@Test
public void rebaseOntoAbandonedChange() throws Exception {
// Create two changes both with the same parent
PushOneCommit.Result r = createChange();
testRepo.reset("HEAD~1");
PushOneCommit.Result r2 = createChange();
// Abandon the first change
String changeId = r.getChangeId();
assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
gApi.changes().id(changeId).abandon();
ChangeInfo info = info(changeId);
assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
RebaseInput ri = new RebaseInput();
ri.base = r.getCommit().name();
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri));
assertThat(thrown).hasMessageThat().contains("base change is abandoned: " + changeId);
}
@Test
public void rebaseOntoSelf() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String commit = r.getCommit().name();
RebaseInput ri = new RebaseInput();
ri.base = commit;
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(changeId).revision(commit).rebase(ri));
assertThat(thrown).hasMessageThat().contains("cannot rebase change onto itself");
}
@Test
@TestProjectInput(createEmptyCommit = false)
public void changeNoParentToOneParent() throws Exception {
// create initial commit with no parent and push it as change, so that patch
// set 1 has no parent
RevCommit c = testRepo.commit().message("Initial commit").insertChangeId().create();
String id = GitUtil.getChangeId(testRepo, c).get();
testRepo.reset(c);
PushResult pr = pushHead(testRepo, "refs/for/master", false);
assertPushOk(pr, "refs/for/master");
ChangeInfo change = gApi.changes().id(id).get();
assertThat(change.revisions.get(change.currentRevision).commit.parents).isEmpty();
// create another initial commit with no parent and push it directly into
// the remote repository
c = testRepo.amend(c.getId()).message("Initial Empty Commit").create();
testRepo.reset(c);
pr = pushHead(testRepo, "refs/heads/master", false);
assertPushOk(pr, "refs/heads/master");
// create a successor commit and push it as second patch set to the change,
// so that patch set 2 has 1 parent
RevCommit c2 =
testRepo
.commit()
.message("Initial commit")
.parent(c)
.insertChangeId(id.substring(1))
.create();
testRepo.reset(c2);
pr = pushHead(testRepo, "refs/for/master", false);
assertPushOk(pr, "refs/for/master");
change = gApi.changes().id(id).get();
RevisionInfo rev = change.revisions.get(change.currentRevision);
assertThat(rev.commit.parents).hasSize(1);
assertThat(rev.commit.parents.get(0).commit).isEqualTo(c.name());
// check that change kind is correctly detected as REWORK
assertThat(rev.kind).isEqualTo(ChangeKind.REWORK);
}
@Test
public void pushCommitOfOtherUser() throws Exception {
// admin pushes commit of user
PushOneCommit push = pushFactory.create(user.newIdent(), testRepo);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
assertThat(change.owner._accountId).isEqualTo(admin.id().get());
CommitInfo commit = change.revisions.get(change.currentRevision).commit;
assertThat(commit.author.email).isEqualTo(user.email());
assertThat(commit.committer.email).isEqualTo(user.email());
// check that the author/committer was added as cc
Collection<AccountInfo> reviewers = change.reviewers.get(CC);
assertThat(reviewers).isNotNull();
assertThat(reviewers).hasSize(1);
assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
assertThat(change.reviewers.get(REVIEWER)).isNull();
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.from().name()).isEqualTo("Administrator (Code Review)");
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
assertThat(m.body()).contains("has uploaded this change for review");
assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
assertMailReplyTo(m, admin.email());
}
@Test
public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception {
// create hidden project that is only visible to administrators
Project.NameKey p = projectOperations.newProject().create();
projectOperations
.project(p)
.forUpdate()
.add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
// admin pushes commit of user
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
PushOneCommit push = pushFactory.create(user.newIdent(), repo);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
assertThat(change.owner._accountId).isEqualTo(admin.id().get());
CommitInfo commit = change.revisions.get(change.currentRevision).commit;
assertThat(commit.author.email).isEqualTo(user.email());
assertThat(commit.committer.email).isEqualTo(user.email());
// check the user cannot see the change
requestScopeOperations.setApiUser(user.id());
assertThrows(
ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
// check that the author/committer was NOT added as reviewer (he can't see
// the change)
assertThat(change.reviewers.get(REVIEWER)).isNull();
assertThat(change.reviewers.get(CC)).isNull();
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void pushCommitWithFooterOfOtherUser() throws Exception {
// admin pushes commit that references 'user' in a footer
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT
+ "\n\n"
+ FooterConstants.REVIEWED_BY.getName()
+ ": "
+ user.newIdent().toExternalString(),
PushOneCommit.FILE_NAME,
PushOneCommit.FILE_CONTENT);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
// check that 'user' was added as reviewer
ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
assertThat(reviewers).isNotNull();
assertThat(reviewers).hasSize(1);
assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
assertThat(change.reviewers.get(CC)).isNull();
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
assertThat(m.body()).contains("Hello " + user.fullName() + ",\n");
assertThat(m.body()).contains("I'd like you to do a code review.");
assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
assertMailReplyTo(m, admin.email());
}
@Test
public void pushCommitWithFooterOfOtherUserThatCannotSeeChange() throws Exception {
// create hidden project that is only visible to administrators
Project.NameKey p = projectOperations.newProject().create();
projectOperations
.project(p)
.forUpdate()
.add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
// admin pushes commit that references 'user' in a footer
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
repo,
PushOneCommit.SUBJECT
+ "\n\n"
+ FooterConstants.REVIEWED_BY.getName()
+ ": "
+ user.newIdent().toExternalString(),
PushOneCommit.FILE_NAME,
PushOneCommit.FILE_CONTENT);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
// check that 'user' cannot see the change
requestScopeOperations.setApiUser(user.id());
assertThrows(
ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
// check that 'user' was NOT added as cc ('user' can't see the change)
requestScopeOperations.setApiUser(admin.id());
ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
assertThat(change.reviewers.get(REVIEWER)).isNull();
assertThat(change.reviewers.get(CC)).isNull();
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void addReviewerThatCannotSeeChange() throws Exception {
// create hidden project that is only visible to administrators
Project.NameKey p = projectOperations.newProject().create();
projectOperations
.project(p)
.forUpdate()
.add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
// create change
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
// check the user cannot see the change
requestScopeOperations.setApiUser(user.id());
assertThrows(
ResourceNotFoundException.class, () -> gApi.changes().id(result.getChangeId()).get());
// try to add user as reviewer
requestScopeOperations.setApiUser(admin.id());
ReviewerInput in = new ReviewerInput();
in.reviewer = user.email();
ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
assertThat(r.input).isEqualTo(user.email());
assertThat(r.error).contains("does not have permission to see this change");
assertThat(r.reviewers).isNull();
}
@Test
public void addReviewerThatIsInactiveByUsername() throws Exception {
PushOneCommit.Result result = createChange();
String username = name("new-user");
Account.Id id = accountOperations.newAccount().username(username).inactive().create();
ReviewerInput in = new ReviewerInput();
in.reviewer = username;
ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
assertThat(r.input).isEqualTo(in.reviewer);
assertThat(r.error).isNull();
assertThat(r.reviewers).hasSize(1);
ReviewerInfo reviewer = r.reviewers.get(0);
assertThat(reviewer._accountId).isEqualTo(id.get());
assertThat(reviewer.username).isEqualTo(username);
}
@Test
public void addReviewerThatIsInactiveById() throws Exception {
PushOneCommit.Result result = createChange();
String username = name("new-user");
Account.Id id = accountOperations.newAccount().username(username).inactive().create();
ReviewerInput in = new ReviewerInput();
in.reviewer = Integer.toString(id.get());
ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
assertThat(r.input).isEqualTo(in.reviewer);
assertThat(r.error).isNull();
assertThat(r.reviewers).hasSize(1);
ReviewerInfo reviewer = r.reviewers.get(0);
assertThat(reviewer._accountId).isEqualTo(id.get());
assertThat(reviewer.username).isEqualTo(username);
}
@Test
public void addReviewerThatIsInactiveByEmail() throws Exception {
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
PushOneCommit.Result result = createChange();
String username = "user@domain.com";
Account.Id id = accountOperations.newAccount().username(username).inactive().create();
ReviewerInput in = new ReviewerInput();
in.reviewer = username;
in.state = ReviewerState.CC;
ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
assertThat(r.input).isEqualTo(username);
assertThat(r.error).isNull();
assertThat(r.ccs).hasSize(1);
AccountInfo reviewer = r.ccs.get(0);
assertThat(reviewer._accountId).isEqualTo(id.get());
assertThat(reviewer.username).isEqualTo(username);
}
@Test
@UseClockStep
public void addReviewer() throws Exception {
testAddReviewerViaPostReview(
(changeId, reviewer) -> {
ReviewerInput in = new ReviewerInput();
in.reviewer = reviewer;
gApi.changes().id(changeId).addReviewer(in);
});
}
@Test
@UseClockStep
public void addReviewerViaPostReview() throws Exception {
testAddReviewerViaPostReview(
(changeId, reviewer) -> {
ReviewerInput reviewerInput = new ReviewerInput();
reviewerInput.reviewer = reviewer;
ReviewInput reviewInput = new ReviewInput();
reviewInput.reviewers = ImmutableList.of(reviewerInput);
gApi.changes().id(changeId).current().review(reviewInput);
});
}
private void testAddReviewerViaPostReview(AddReviewerCaller addReviewer) throws Exception {
PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag();
Instant oldTs = rsrc.getChange().getLastUpdatedOn();
addReviewer.call(r.getChangeId(), user.email());
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
assertThat(m.body()).contains("Hello " + user.fullName() + ",\n");
assertThat(m.body()).contains("I'd like you to do a code review.");
assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
assertMailReplyTo(m, admin.email());
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
// Adding a reviewer records that user as reviewer.
Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
assertThat(reviewers).isNotNull();
assertThat(reviewers).hasSize(1);
assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
// Nobody was added as CC.
assertThat(c.reviewers.get(CC)).isNull();
// Ensure ETag and lastUpdatedOn are updated.
rsrc = parseResource(r);
assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
// Change status of reviewer and ensure ETag is updated.
oldETag = rsrc.getETag();
accountOperations.account(user.id()).forUpdate().status("new status").update();
rsrc = parseResource(r);
assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
}
@Test
public void postingMessageOnOwnChangeDoesntAddCallerAsReviewer() throws Exception {
PushOneCommit.Result r = createChange();
ReviewInput reviewInput = new ReviewInput();
reviewInput.message = "Foo Bar";
gApi.changes().id(r.getChangeId()).current().review(reviewInput);
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
assertThat(c.reviewers.get(REVIEWER)).isNull();
assertThat(c.reviewers.get(CC)).isNull();
}
@Test
public void listReviewers() throws Exception {
PushOneCommit.Result r = createChange();
ReviewerInput in = new ReviewerInput();
in.reviewer = user.email();
gApi.changes().id(r.getChangeId()).addReviewer(in);
assertThat(gApi.changes().id(r.getChangeId()).reviewers()).hasSize(1);
String username1 = name("user1");
String email1 = username1 + "@example.com";
accountOperations
.newAccount()
.username(username1)
.preferredEmail(email1)
.fullname("User1")
.create();
in.reviewer = email1;
in.state = ReviewerState.CC;
gApi.changes().id(r.getChangeId()).addReviewer(in);
assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a.username))
.containsExactly(user.username(), username1);
}
@Test
public void notificationsForAddedWorkInProgressReviewers() throws Exception {
ReviewerInput in = new ReviewerInput();
in.reviewer = user.email();
ReviewInput batchIn = new ReviewInput();
batchIn.reviewers = ImmutableList.of(in);
// Added reviewers not notified by default.
PushOneCommit.Result r = createWorkInProgressChange();
gApi.changes().id(r.getChangeId()).addReviewer(in);
assertThat(sender.getMessages()).isEmpty();
// Default notification handling can be overridden.
r = createWorkInProgressChange();
in.notify = NotifyHandling.OWNER_REVIEWERS;
gApi.changes().id(r.getChangeId()).addReviewer(in);
assertThat(sender.getMessages()).hasSize(1);
sender.clear();
// Reviewers added via PostReview also not notified by default.
// In this case, the child ReviewerInput has a notify=OWNER_REVIEWERS
// that should be ignored.
r = createWorkInProgressChange();
gApi.changes().id(r.getChangeId()).current().review(batchIn);
assertThat(sender.getMessages()).isEmpty();
// Top-level notify property can force notifications when adding reviewer
// via PostReview.
r = createWorkInProgressChange();
batchIn.notify = NotifyHandling.OWNER_REVIEWERS;
gApi.changes().id(r.getChangeId()).current().review(batchIn);
assertThat(sender.getMessages()).hasSize(1);
}
@Test
@UseClockStep
public void addReviewerThatIsNotPerfectMatch() throws Exception {
PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag();
Instant oldTs = rsrc.getChange().getLastUpdatedOn();
// create a group named "ab" with one user: testUser
String email = "abcd@example.com";
String fullname = "abcd";
Account.Id accountIdOfTestUser =
accountOperations
.newAccount()
.username("abcd")
.preferredEmail(email)
.fullname(fullname)
.create();
String testGroup = groupOperations.newGroup().name("ab").create().get();
GroupApi groupApi = gApi.groups().id(testGroup);
groupApi.description("test group");
groupApi.addMembers(user.fullName());
ReviewerInput in = new ReviewerInput();
in.reviewer = "abc";
gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer);
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(Address.create(fullname, email));
assertThat(m.body()).contains("Hello " + fullname + ",\n");
assertThat(m.body()).contains("I'd like you to do a code review.");
assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
assertMailReplyTo(m, email);
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
// Adding a reviewer records that user as reviewer.
Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
assertThat(reviewers).isNotNull();
assertThat(reviewers).hasSize(1);
assertThat(reviewers.iterator().next()._accountId).isEqualTo(accountIdOfTestUser.get());
// Ensure ETag and lastUpdatedOn are updated.
rsrc = parseResource(r);
assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
}
@Test
@UseClockStep
public void addGroupAsReviewersWhenANotPerfectMatchedUserExists() throws Exception {
PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag();
Instant oldTs = rsrc.getChange().getLastUpdatedOn();
// create a group named "kobe" with one user: lee
String testUserFullname = "kobebryant";
accountOperations
.newAccount()
.username("kobebryant")
.preferredEmail("kobebryant@example.com")
.fullname(testUserFullname)
.create();
String myGroupUserEmail = "lee@example.com";
String myGroupUserFullname = "lee";
Account.Id accountIdOfGroupUser =
accountOperations
.newAccount()
.username("lee")
.preferredEmail(myGroupUserEmail)
.fullname(myGroupUserFullname)
.create();
String testGroup = groupOperations.newGroup().name("kobe").create().get();
GroupApi groupApi = gApi.groups().id(testGroup);
groupApi.description("test group");
groupApi.addMembers(myGroupUserFullname);
// ensure that user "user" is not in the group
groupApi.removeMembers(testUserFullname);
ReviewerInput in = new ReviewerInput();
in.reviewer = testGroup;
gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer);
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
assertThat(m.rcpt()).containsExactly(Address.create(myGroupUserFullname, myGroupUserEmail));
assertThat(m.body()).contains("Hello " + myGroupUserFullname + ",\n");
assertThat(m.body()).contains("I'd like you to do a code review.");
assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
assertMailReplyTo(m, myGroupUserEmail);
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
// Adding a reviewer records that user as reviewer.
Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
assertThat(reviewers).isNotNull();
assertThat(reviewers).hasSize(1);
assertThat(reviewers.iterator().next()._accountId).isEqualTo(accountIdOfGroupUser.get());
// Ensure ETag and lastUpdatedOn are updated.
rsrc = parseResource(r);
assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
}
@Test
public void deleteGroupFromReviewersFails() throws Exception {
PushOneCommit.Result r = createChange();
// create a group named "kobe" with one user: lee
String myGroupUserEmail = "lee@example.com";
String myGroupUserFullname = "lee";
accountOperations
.newAccount()
.username("lee")
.preferredEmail(myGroupUserEmail)
.fullname(myGroupUserFullname)
.create();
String groupName = "kobe";
String testGroup = groupOperations.newGroup().name(groupName).create().get();
GroupApi groupApi = gApi.groups().id(testGroup);
groupApi.description("test group");
groupApi.addMembers(myGroupUserFullname);
// add the user as reviewer.
gApi.changes().id(r.getChangeId()).addReviewer(myGroupUserFullname);
// fail to remove that user via group.
ReviewResult reviewResult =
gApi.changes()
.id(r.getChangeId())
.current()
.review(ReviewInput.create().reviewer(testGroup, REMOVED, /* confirmed= */ true));
assertThat(reviewResult.error).isEqualTo("error adding reviewer");
ReviewerInput in = new ReviewerInput();
in.reviewer = testGroup;
in.state = REMOVED;
ReviewerResult reviewerResult = gApi.changes().id(r.getChangeId()).addReviewer(in);
assertThat(reviewerResult.error)
.isEqualTo(MessageFormat.format(ChangeMessages.get().groupRemovalIsNotAllowed, groupName));
}
@Test
@UseClockStep
public void addSelfAsReviewer() throws Exception {
PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag();
Instant oldTs = rsrc.getChange().getLastUpdatedOn();
ReviewerInput in = new ReviewerInput();
in.reviewer = user.email();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(r.getChangeId()).addReviewer(in);
// There should be no email notification when adding self
assertThat(sender.getMessages()).isEmpty();
// Adding a reviewer records that user as reviewer.
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
assertThat(reviewers).isNotNull();
assertThat(reviewers).hasSize(1);
assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.id().get());
// Ensure ETag and lastUpdatedOn are updated.
rsrc = parseResource(r);
assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs);
}
@Test
public void implicitlyCcOnNonVotingReviewPgStyle() throws Exception {
testImplicitlyCcOnNonVotingReviewPgStyle(user);
}
@Test
public void implicitlyCcOnNonVotingReviewForUserWithoutUserNamePgStyle() throws Exception {
com.google.gerrit.acceptance.TestAccount accountWithoutUsername = accountCreator.create();
assertThat(accountWithoutUsername.username()).isNull();
testImplicitlyCcOnNonVotingReviewPgStyle(accountWithoutUsername);
}
private void testImplicitlyCcOnNonVotingReviewPgStyle(
com.google.gerrit.acceptance.TestAccount testAccount) throws Exception {
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(testAccount.id());
assertThat(getReviewerState(r.getChangeId(), testAccount.id())).isEmpty();
// Exact request format made by PG UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
ReviewInput in = new ReviewInput();
in.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
in.labels = ImmutableMap.of();
in.message = "comment";
in.reviewers = ImmutableList.of();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
assertThat(getReviewerState(r.getChangeId(), testAccount.id())).hasValue(CC);
}
@Test
public void implicitlyAddReviewerOnVotingReview() throws Exception {
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(ReviewInput.recommend().message("LGTM"));
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
assertThat(c.reviewers.get(REVIEWER).stream().map(ai -> ai._accountId).collect(toList()))
.containsExactly(user.id().get());
// Further test: remove the vote, then comment again. The user should be
// implicitly re-added to the ReviewerSet, as a CC.
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).remove();
c = gApi.changes().id(r.getChangeId()).get();
assertThat(c.reviewers.values()).isEmpty();
requestScopeOperations.setApiUser(user.id());
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(new ReviewInput().message("hi"));
c = gApi.changes().id(r.getChangeId()).get();
assertThat(c.reviewers.get(CC).stream().map(ai -> ai._accountId).collect(toList()))
.containsExactly(user.id().get());
}
@Test
public void addReviewerToClosedChange() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
assertThat(reviewers).hasSize(1);
assertThat(reviewers.iterator().next()._accountId).isEqualTo(admin.id().get());
assertThat(c.reviewers).doesNotContainKey(CC);
ReviewerInput in = new ReviewerInput();
in.reviewer = user.email();
gApi.changes().id(r.getChangeId()).addReviewer(in);
c = gApi.changes().id(r.getChangeId()).get();
reviewers = c.reviewers.get(REVIEWER);
assertThat(reviewers).hasSize(2);
Iterator<AccountInfo> reviewerIt = reviewers.iterator();
assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get());
assertThat(reviewerIt.next()._accountId).isEqualTo(user.id().get());
assertThat(c.reviewers).doesNotContainKey(CC);
}
@Test
public void eTagChangesWhenOwnerUpdatesAccountStatus() throws Exception {
PushOneCommit.Result r = createChange();
ChangeResource rsrc = parseResource(r);
String oldETag = rsrc.getETag();
accountOperations.account(admin.id()).forUpdate().status("new status").update();
rsrc = parseResource(r);
assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
}
@Test
public void pluginCanContributeToETagComputation() throws Exception {
PushOneCommit.Result r = createChange();
String oldETag = parseResource(r).getETag();
try (Registration registration =
extensionRegistry.newRegistration().add(TestChangeETagComputation.withETag("foo"))) {
assertThat(parseResource(r).getETag()).isNotEqualTo(oldETag);
}
assertThat(parseResource(r).getETag()).isEqualTo(oldETag);
}
@Test
public void returningNullFromETagComputationDoesNotBreakGerrit() throws Exception {
PushOneCommit.Result r = createChange();
String oldETag = parseResource(r).getETag();
try (Registration registration =
extensionRegistry.newRegistration().add(TestChangeETagComputation.withETag(null))) {
assertThat(parseResource(r).getETag()).isEqualTo(oldETag);
}
}
@Test
public void throwingExceptionFromETagComputationDoesNotBreakGerrit() throws Exception {
PushOneCommit.Result r = createChange();
String oldETag = parseResource(r).getETag();
try (Registration registration =
extensionRegistry
.newRegistration()
.add(
TestChangeETagComputation.withException(
new StorageException("exception during test")))) {
assertThat(parseResource(r).getETag()).isEqualTo(oldETag);
}
}
@Test
public void emailNotificationForFileLevelComment() throws Exception {
String changeId = createChange().getChangeId();
ReviewerInput in = new ReviewerInput();
in.reviewer = user.email();
gApi.changes().id(changeId).addReviewer(in);
sender.clear();
ReviewInput review = new ReviewInput();
ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
comment.path = PushOneCommit.FILE_NAME;
comment.side = Side.REVISION;
comment.message = "comment 1";
review.comments = new HashMap<>();
review.comments.put(comment.path, Lists.newArrayList(comment));
gApi.changes().id(changeId).current().review(review);
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
assertThat(m.rcpt()).containsExactly(user.getNameEmail());
}
@Test
public void invalidRange() throws Exception {
String changeId = createChange().getChangeId();
ReviewInput review = new ReviewInput();
ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
comment.range = new Range();
comment.range.startLine = 1;
comment.range.endLine = 1;
comment.range.startCharacter = -1;
comment.range.endCharacter = 0;
comment.path = PushOneCommit.FILE_NAME;
comment.side = Side.REVISION;
comment.message = "comment 1";
review.comments = ImmutableMap.of(comment.path, Lists.newArrayList(comment));
assertThrows(
BadRequestException.class, () -> gApi.changes().id(changeId).current().review(review));
}
@Test
public void listVotes() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
Map<String, Short> m =
gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).votes();
assertThat(m).hasSize(1);
assertThat(m).containsExactly(LabelId.CODE_REVIEW, Short.valueOf((short) 2));
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.dislike());
m = gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).votes();
assertThat(m).hasSize(1);
assertThat(m).containsExactly(LabelId.CODE_REVIEW, Short.valueOf((short) -1));
}
@Test
@GerritConfig(name = "accounts.visibility", value = "NONE")
public void listVotesEvenWhenAccountsAreNotVisible() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
requestScopeOperations.setApiUser(user.id());
// check finding by address works
Map<String, Short> m = gApi.changes().id(r.getChangeId()).reviewer(admin.email()).votes();
assertThat(m).hasSize(1);
assertThat(m).containsEntry(LabelId.CODE_REVIEW, Short.valueOf((short) 2));
// check finding by id works
m = gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).votes();
assertThat(m).hasSize(1);
assertThat(m).containsEntry(LabelId.CODE_REVIEW, Short.valueOf((short) 2));
}
@Test
public void removeReviewerNoVotes() throws Exception {
LabelType verified =
label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().upsertLabelType(verified);
u.save();
}
projectOperations
.project(project)
.forUpdate()
.add(
allowLabel(verified.getName())
.ref(RefNames.REFS_HEADS + "*")
.group(REGISTERED_USERS)
.range(-1, 1))
.update();
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
gApi.changes().id(changeId).addReviewer(user.id().toString());
ChangeInfo c = gApi.changes().id(changeId).get(ListChangesOption.DETAILED_LABELS);
assertThat(getReviewers(c.reviewers.get(CC))).isEmpty();
assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.id());
sender.clear();
gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
assertThat(sender.getMessages()).hasSize(1);
Message message = sender.getMessages().get(0);
assertThat(message.body()).contains("Removed reviewer " + user.getNameEmail() + ".");
assertThat(message.body()).doesNotContain("with the following votes");
// Make sure the change message for removing a reviewer is correct.
assertThat(Iterables.getLast(gApi.changes().id(changeId).messages()).message)
.isEqualTo("Removed reviewer " + user.getNameEmail() + ".");
ChangeMessageInfo changeMessageInfo =
Iterables.getLast(gApi.changes().id(changeId).get().messages);
assertThat(changeMessageInfo.message)
.isEqualTo("Removed reviewer " + AccountTemplateUtil.getAccountTemplate(user.id()) + ".");
assertThat(changeMessageInfo.accountsInMessage).containsExactly(getAccountInfo(user.id()));
// Make sure the reviewer can still be added again.
gApi.changes().id(changeId).addReviewer(user.id().toString());
c = gApi.changes().id(changeId).get();
assertThat(getReviewers(c.reviewers.get(CC))).isEmpty();
assertThat(getReviewers(c.reviewers.get(REVIEWER))).containsExactly(user.id());
// Remove again, and then try to remove once more to verify 404 is
// returned.
gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
assertThrows(
ResourceNotFoundException.class,
() -> gApi.changes().id(changeId).reviewer(user.id().toString()).remove());
}
@Test
public void removeChangeOwnerAsReviewerByDelete() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
// vote on the change so that the change owner becomes a reviewer
approve(changeId);
assertThat(getReviewers(gApi.changes().id(changeId).get().reviewers.get(REVIEWER)))
.containsExactly(admin.id());
gApi.changes().id(changeId).reviewer(admin.id().toString()).remove();
assertThat(getReviewers(gApi.changes().id(changeId).get().reviewers.get(REVIEWER))).isEmpty();
}
@Test
public void removeChangeOwnerAsReviewerByPostReview() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
// vote on the change so that the change owner becomes a reviewer
approve(changeId);
assertThat(getReviewers(gApi.changes().id(changeId).get().reviewers.get(REVIEWER)))
.containsExactly(admin.id());
ReviewInput reviewInput = new ReviewInput();
reviewInput.reviewer(admin.id().toString(), ReviewerState.REMOVED, /* confirmed= */ false);
gApi.changes().id(changeId).current().review(reviewInput);
assertThat(getReviewers(gApi.changes().id(changeId).get().reviewers.get(REVIEWER))).isEmpty();
}
@Test
public void removeCC() throws Exception {
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
// Add a cc
ReviewerInput reviewerInput = new ReviewerInput();
reviewerInput.state = CC;
reviewerInput.reviewer = user.id().toString();
gApi.changes().id(changeId).addReviewer(reviewerInput);
// Remove a cc
sender.clear();
gApi.changes().id(changeId).reviewer(user.id().toString()).remove();
assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
// Make sure the email for removing a cc is correct.
assertThat(sender.getMessages()).hasSize(1);
Message message = sender.getMessages().get(0);
assertThat(message.body()).contains("Removed cc " + user.getNameEmail() + ".");
// Make sure the change message for removing a reviewer is correct.
assertThat(Iterables.getLast(gApi.changes().id(changeId).messages()).message)
.isEqualTo("Removed cc " + user.getNameEmail() + ".");
ChangeMessageInfo changeMessageInfo =
Iterables.getLast(gApi.changes().id(changeId).get().messages);
assertThat(changeMessageInfo.message)
.isEqualTo("Removed cc " + AccountTemplateUtil.getAccountTemplate(user.id()) + ".");
assertThat(changeMessageInfo.accountsInMessage).containsExactly(getAccountInfo(user.id()));
}
@Test
public void removeReviewer() throws Exception {
testRemoveReviewer(true);
}
@Test
public void removeNoNotify() throws Exception {
testRemoveReviewer(false);
}
private void testRemoveReviewer(boolean notify) throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.recommend());
Collection<AccountInfo> reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
assertThat(reviewers).hasSize(2);
Iterator<AccountInfo> reviewerIt = reviewers.iterator();
assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get());
assertThat(reviewerIt.next()._accountId).isEqualTo(user.id().get());
sender.clear();
requestScopeOperations.setApiUser(admin.id());
DeleteReviewerInput input = new DeleteReviewerInput();
if (!notify) {
input.notify = NotifyHandling.NONE;
}
gApi.changes().id(changeId).reviewer(user.id().toString()).remove(input);
if (notify) {
assertThat(sender.getMessages()).hasSize(1);
Message message = sender.getMessages().get(0);
assertThat(message.body())
.contains("Removed reviewer " + user.getNameEmail() + " with the following votes");
assertThat(message.body()).contains("* Code-Review+1 by " + user.getNameEmail());
ChangeMessageInfo changeMessageInfo =
Iterables.getLast(gApi.changes().id(changeId).messages());
assertThat(changeMessageInfo.message)
.contains("Removed reviewer " + user.getNameEmail() + " with the following votes");
assertThat(changeMessageInfo.message).contains("* Code-Review+1 by " + user.getNameEmail());
changeMessageInfo = Iterables.getLast(gApi.changes().id(changeId).get().messages);
assertThat(changeMessageInfo.message)
.contains(
"Removed reviewer "
+ AccountTemplateUtil.getAccountTemplate(user.id())
+ " with the following votes");
assertThat(changeMessageInfo.message)
.contains("* Code-Review+1 by " + AccountTemplateUtil.getAccountTemplate(user.id()));
assertThat(changeMessageInfo.accountsInMessage).containsExactly(getAccountInfo(user.id()));
} else {
assertThat(sender.getMessages()).isEmpty();
}
reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER);
assertThat(reviewers).hasSize(1);
reviewerIt = reviewers.iterator();
assertThat(reviewerIt.next()._accountId).isEqualTo(admin.id().get());
eventRecorder.assertReviewerDeletedEvents(changeId, user.email());
}
@Test
public void removeReviewerNotPermitted() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
AuthException.class,
() -> gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove());
assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
}
@Test
public void removeReviewerSelfFromMergedChangeNotPermitted() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
requestScopeOperations.setApiUser(user.id());
recommend(changeId);
requestScopeOperations.setApiUser(admin.id());
approve(changeId);
gApi.changes().id(changeId).revision(r.getCommit().name()).submit();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
AuthException.class,
() -> gApi.changes().id(r.getChangeId()).reviewer("self").remove());
assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
}
@Test
public void removeReviewerSelfFromAbandonedChangePermitted() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
requestScopeOperations.setApiUser(user.id());
recommend(changeId);
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(changeId).abandon();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(r.getChangeId()).reviewer("self").remove();
eventRecorder.assertReviewerDeletedEvents(changeId, user.email());
}
@Test
public void removeOtherReviewerFromAbandonedChangeNotPermitted() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
requestScopeOperations.setApiUser(user.id());
recommend(changeId);
requestScopeOperations.setApiUser(admin.id());
approve(changeId);
gApi.changes().id(changeId).abandon();
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
AuthException.class,
() -> gApi.changes().id(r.getChangeId()).reviewer(admin.id().toString()).remove());
assertThat(thrown).hasMessageThat().contains("remove reviewer not permitted");
}
@Test
public void deleteVote() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
requestScopeOperations.setApiUser(user.id());
recommend(r.getChangeId());
requestScopeOperations.setApiUser(admin.id());
sender.clear();
gApi.changes()
.id(r.getChangeId())
.reviewer(user.id().toString())
.deleteVote(LabelId.CODE_REVIEW);
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message msg = messages.get(0);
assertThat(msg.rcpt()).containsExactly(user.getNameEmail());
assertThat(msg.body()).contains(admin.fullName() + " has removed a vote from this change.");
assertThat(msg.body())
.contains("Removed Code-Review+1 by " + user.fullName() + " <" + user.email() + ">\n");
Map<String, Short> m =
gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).votes();
// Dummy 0 approval on the change to block vote copying to this patch set.
assertThat(m).containsExactly(LabelId.CODE_REVIEW, Short.valueOf((short) 0));
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
ChangeMessageInfo message = Iterables.getLast(c.messages);
assertThat(message.author._accountId).isEqualTo(admin.id().get());
assertThat(message.message)
.isEqualTo(
"Removed Code-Review+1 by " + AccountTemplateUtil.getAccountTemplate(user.id()) + "\n");
assertThat(message.accountsInMessage).containsExactly(getAccountInfo(user.id()));
assertThat(gApi.changes().id(r.getChangeId()).message(message.id).get().message)
.isEqualTo("Removed Code-Review+1 by User1 <user1@example.com>\n");
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
.containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
}
@Test
public void deleteVoteNotifyNone() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
requestScopeOperations.setApiUser(user.id());
recommend(r.getChangeId());
requestScopeOperations.setApiUser(admin.id());
sender.clear();
DeleteVoteInput in = new DeleteVoteInput();
in.label = LabelId.CODE_REVIEW;
in.notify = NotifyHandling.NONE;
gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
assertThat(sender.getMessages()).isEmpty();
}
@Test
public void deleteVoteWithReason() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
requestScopeOperations.setApiUser(user.id());
recommend(r.getChangeId());
requestScopeOperations.setApiUser(admin.id());
sender.clear();
DeleteVoteInput in = new DeleteVoteInput();
in.label = LabelId.CODE_REVIEW;
in.reason = "Internal conflict resolved";
gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
assertThat(Iterables.getLast(gApi.changes().id(r.getChangeId()).messages()).message)
.isEqualTo(
"Removed Code-Review+1 by User1 <user1@example.com>\n"
+ "\n"
+ "Internal conflict resolved\n");
}
@Test
public void deleteVoteNotifyAccount() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
DeleteVoteInput in = new DeleteVoteInput();
in.label = LabelId.CODE_REVIEW;
in.notify = NotifyHandling.NONE;
// notify unrelated account as TO
String email = "user2@example.com";
accountOperations
.newAccount()
.username("user2")
.preferredEmail(email)
.fullname("User2")
.create();
requestScopeOperations.setApiUser(user.id());
recommend(r.getChangeId());
requestScopeOperations.setApiUser(admin.id());
sender.clear();
in.notifyDetails = new HashMap<>();
in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(email)));
gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
assertNotifyTo(email, "User2");
// notify unrelated account as CC
requestScopeOperations.setApiUser(user.id());
recommend(r.getChangeId());
requestScopeOperations.setApiUser(admin.id());
sender.clear();
in.notifyDetails = new HashMap<>();
in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(email)));
gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
assertNotifyCc(email, "User2");
// notify unrelated account as BCC
requestScopeOperations.setApiUser(user.id());
recommend(r.getChangeId());
requestScopeOperations.setApiUser(admin.id());
sender.clear();
in.notifyDetails = new HashMap<>();
in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(email)));
gApi.changes().id(r.getChangeId()).reviewer(user.id().toString()).deleteVote(in);
assertNotifyBcc(email, "User2");
}
@Test
public void deleteVoteNotPermitted() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
AuthException.class,
() ->
gApi.changes()
.id(r.getChangeId())
.reviewer(admin.id().toString())
.deleteVote(LabelId.CODE_REVIEW));
assertThat(thrown).hasMessageThat().contains("delete vote not permitted");
}
@Test
public void nonVotingReviewerStaysAfterSubmit() throws Exception {
LabelType verified =
label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
String heads = "refs/heads/*";
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().upsertLabelType(verified);
u.save();
}
projectOperations
.project(project)
.forUpdate()
.add(allowLabel(verified.getName()).ref(heads).group(CHANGE_OWNER).range(-1, 1))
.add(allowLabel(LabelId.CODE_REVIEW).ref(heads).group(REGISTERED_USERS).range(-2, +2))
.update();
// Set Code-Review+2 and Verified+1 as admin (change owner)
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String commit = r.getCommit().name();
ReviewInput input = ReviewInput.approve();
input.label(verified.getName(), 1);
gApi.changes().id(changeId).revision(commit).review(input);
// Reviewers should only be "admin"
ChangeInfo c = gApi.changes().id(changeId).get();
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
.containsExactlyElementsIn(ImmutableSet.of(admin.id()));
assertThat(c.reviewers.get(CC)).isNull();
// Add the user as reviewer
ReviewerInput in = new ReviewerInput();
in.reviewer = user.email();
gApi.changes().id(changeId).addReviewer(in);
c = gApi.changes().id(changeId).get();
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
.containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
// Approve the change as user, then remove the approval
// (only to confirm that the user does have Code-Review+2 permission)
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(changeId).revision(commit).review(ReviewInput.approve());
gApi.changes().id(changeId).revision(commit).review(ReviewInput.noScore());
// Submit the change
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(changeId).revision(commit).submit();
// User should still be on the change
c = gApi.changes().id(changeId).get();
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
.containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
}
@Test
public void labelPermissionsChange_doesNotAffectCurrentVotes() throws Exception {
String heads = "refs/heads/*";
projectOperations
.project(project)
.forUpdate()
.add(allowLabel(LabelId.CODE_REVIEW).ref(heads).group(REGISTERED_USERS).range(-2, +2))
.update();
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
// Approve the change as user
requestScopeOperations.setApiUser(user.id());
approve(changeId);
assertThat(
gApi.changes().id(changeId).get(DETAILED_LABELS).labels.get("Code-Review").all.stream()
.collect(toImmutableMap(vote -> Account.id(vote._accountId), vote -> vote.value)))
.isEqualTo(ImmutableMap.of(user.id(), 2));
// Remove permissions for CODE_REVIEW. The user still has [-1,+1], inherited from All-Projects.
projectOperations
.project(project)
.forUpdate()
.remove(labelPermissionKey(LabelId.CODE_REVIEW).ref(heads).group(REGISTERED_USERS))
.update();
// No permissions to vote +2
assertThrows(AuthException.class, () -> approve(changeId));
assertThat(
get(changeId, DETAILED_LABELS).labels.get(LabelId.CODE_REVIEW).all.stream()
.map(vote -> vote.value))
.containsExactly(2);
// The change is still submittable
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(changeId).current().submit();
assertThat(info(changeId).status).isEqualTo(MERGED);
// The +2 vote out of permissions range is still present.
assertThat(
get(changeId, DETAILED_LABELS).labels.get(LabelId.CODE_REVIEW).all.stream()
.collect(toImmutableMap(vote -> Account.id(vote._accountId), vote -> vote.value)))
.isEqualTo(ImmutableMap.of(user.id(), 2, admin.id(), 0));
}
@Test
public void createEmptyChange() throws Exception {
ChangeInput in = new ChangeInput();
in.branch = Constants.MASTER;
in.subject = "Create a change from the API";
in.project = project.get();
ChangeInfo info = gApi.changes().create(in).get();
assertThat(info.project).isEqualTo(in.project);
assertThat(RefNames.fullName(info.branch)).isEqualTo(RefNames.fullName(in.branch));
assertThat(info.subject).isEqualTo(in.subject);
assertThat(Iterables.getOnlyElement(info.messages).message).isEqualTo("Uploaded patch set 1.");
}
@Test
public void queryChangesNoQuery() throws Exception {
PushOneCommit.Result r = createChange();
List<ChangeInfo> results = gApi.changes().query().get();
assertThat(results.size()).isAtLeast(1);
List<Integer> ids = new ArrayList<>(results.size());
for (int i = 0; i < results.size(); i++) {
ChangeInfo info = results.get(i);
if (i == 0) {
assertThat(info._number).isEqualTo(r.getChange().getId().get());
}
assertThat(Change.Status.forChangeStatus(info.status).isOpen()).isTrue();
ids.add(info._number);
}
assertThat(ids).contains(r.getChange().getId().get());
}
@Test
public void queryChangesNoResults() throws Exception {
createChange();
assertThat(query("message:test")).isNotEmpty();
assertThat(query("message:{" + getClass().getName() + "fhqwhgads}")).isEmpty();
}
@Test
public void queryChanges() throws Exception {
PushOneCommit.Result r1 = createChange();
createChange();
List<ChangeInfo> results = query("project:{" + project.get() + "} " + r1.getChangeId());
assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r1.getChangeId());
}
@Test
public void queryChangesLimit() throws Exception {
createChange();
PushOneCommit.Result r2 = createChange();
List<ChangeInfo> results = gApi.changes().query().withLimit(1).get();
assertThat(results).hasSize(1);
assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r2.getChangeId());
}
@Test
public void queryChangesNoLimitRegisteredUser() throws Exception {
projectOperations
.allProjectsForUpdate()
.add(
allowCapability(GlobalCapability.QUERY_LIMIT)
.group(SystemGroupBackend.REGISTERED_USERS)
.range(0, 2))
.update();
for (int i = 0; i < 3; i++) {
createChange();
}
List<ChangeInfo> resultsWithDefaultLimit = gApi.changes().query().get();
List<ChangeInfo> resultsWithNoLimit = gApi.changes().query().withNoLimit().get();
assertThat(resultsWithDefaultLimit).hasSize(2);
assertThat(resultsWithNoLimit.size()).isAtLeast(3);
}
@Test
public void queryChangesNoLimitIgnoredForAnonymousUser() throws Exception {
int limit = 2;
projectOperations
.allProjectsForUpdate()
.add(
allowCapability(GlobalCapability.QUERY_LIMIT)
.group(SystemGroupBackend.ANONYMOUS_USERS)
.range(0, limit))
.update();
for (int i = 0; i < 3; i++) {
createChange();
}
requestScopeOperations.setApiUserAnonymous();
List<ChangeInfo> resultsWithDefaultLimit = gApi.changes().query().get();
List<ChangeInfo> resultsWithNoLimit = gApi.changes().query().withNoLimit().get();
assertThat(resultsWithDefaultLimit).hasSize(limit);
assertThat(resultsWithNoLimit).hasSize(limit);
}
@Test
public void queryChangesStart() throws Exception {
PushOneCommit.Result r1 = createChange();
createChange();
List<ChangeInfo> results =
gApi.changes().query("project:{" + project.get() + "}").withStart(1).get();
assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r1.getChangeId());
}
@Test
public void queryChangesNoOptions() throws Exception {
PushOneCommit.Result r = createChange();
ChangeInfo result = Iterables.getOnlyElement(query(r.getChangeId()));
assertThat(result.labels).isNull();
assertThat(result.messages).isNull();
assertThat(result.revisions).isNull();
assertThat(result.actions).isNull();
}
@Test
public void queryChangesOptions() throws Exception {
PushOneCommit.Result r = createChange();
ChangeInfo result = Iterables.getOnlyElement(gApi.changes().query(r.getChangeId()).get());
assertThat(result.labels).isNull();
assertThat(result.messages).isNull();
assertThat(result.actions).isNull();
assertThat(result.revisions).isNull();
result =
Iterables.getOnlyElement(
gApi.changes()
.query(r.getChangeId())
.withOptions(
ALL_REVISIONS, CHANGE_ACTIONS, CURRENT_ACTIONS, DETAILED_LABELS, MESSAGES)
.get());
assertThat(Iterables.getOnlyElement(result.labels.keySet())).isEqualTo(LabelId.CODE_REVIEW);
assertThat(result.messages).hasSize(1);
assertThat(result.actions).isNotEmpty();
RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values());
assertThat(rev._number).isEqualTo(r.getPatchSetId().get());
assertThat(rev.created).isNotNull();
assertThat(rev.uploader._accountId).isEqualTo(admin.id().get());
assertThat(rev.ref).isEqualTo(r.getPatchSetId().toRefName());
assertThat(rev.actions).isNotEmpty();
}
@Test
public void queryChangesOwnerWithDifferentUsers() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(
Iterables.getOnlyElement(query("project:{" + project.get() + "} owner:self")).changeId)
.isEqualTo(r.getChangeId());
requestScopeOperations.setApiUser(user.id());
assertThat(query("owner:self project:{" + project.get() + "}")).isEmpty();
}
private static class OperatorModule extends AbstractModule {
@Override
public void configure() {
bind(ChangeOperatorFactory.class)
.annotatedWith(Exports.named("mytopic"))
.toInstance((cqb, value) -> new MyTopicPredicate(value));
}
private static class MyTopicPredicate extends PostFilterPredicate<ChangeData> {
MyTopicPredicate(String value) {
super("mytopic", value);
}
@Override
public boolean match(ChangeData cd) {
return Objects.equals(cd.change().getTopic(), value);
}
@Override
public int getCost() {
return 2;
}
}
}
@Test
public void queryChangesPluginOperator() throws Exception {
PushOneCommit.Result r = createChange();
String query = "mytopic_myplugin:foo";
String expectedMessage = "Unsupported operator mytopic_myplugin:foo";
assertThatQueryException(query).hasMessageThat().isEqualTo(expectedMessage);
try (AutoCloseable ignored = installPlugin("myplugin", OperatorModule.class)) {
assertThat(query(query)).isEmpty();
gApi.changes().id(r.getChangeId()).topic("foo");
assertThat(query(query).stream().map(i -> i.changeId)).containsExactly(r.getChangeId());
}
assertThatQueryException(query).hasMessageThat().isEqualTo(expectedMessage);
}
@Test
public void checkReviewedFlagBeforeAndAfterReview() throws Exception {
PushOneCommit.Result r = createChange();
ReviewerInput in = new ReviewerInput();
in.reviewer = user.email();
gApi.changes().id(r.getChangeId()).addReviewer(in);
requestScopeOperations.setApiUser(user.id());
assertThat(get(r.getChangeId(), REVIEWED).reviewed).isNull();
revision(r).review(ReviewInput.recommend());
assertThat(get(r.getChangeId(), REVIEWED).reviewed).isTrue();
}
@Test
public void topic() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
gApi.changes().id(r.getChangeId()).topic("mytopic");
assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic");
gApi.changes().id(r.getChangeId()).topic("");
assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
}
@Test
public void editTopicWithoutPermissionNotAllowed() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
AuthException.class, () -> gApi.changes().id(r.getChangeId()).topic("mytopic"));
assertThat(thrown).hasMessageThat().contains("edit topic name not permitted");
}
@Test
public void editTopicWithPermissionAllowed() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.EDIT_TOPIC_NAME).ref("refs/heads/master").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(r.getChangeId()).topic("mytopic");
assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic");
}
@Test
public void submitted() throws Exception {
PushOneCommit.Result r = createChange();
String id = r.getChangeId();
ChangeInfo c = gApi.changes().id(r.getChangeId()).info();
assertThat(c.submitted).isNull();
assertThat(c.submitter).isNull();
gApi.changes().id(id).current().review(ReviewInput.approve());
gApi.changes().id(id).current().submit();
c = gApi.changes().id(r.getChangeId()).info();
assertThat(c.submitted).isNotNull();
assertThat(c.submitter).isNotNull();
assertThat(c.submitter._accountId).isEqualTo(atrScope.get().getUser().getAccountId().get());
}
@Test
public void submitStaleChange() throws Exception {
PushOneCommit.Result r = createChange();
try (AutoCloseable ignored = changeIndexOperations.disableWrites()) {
r = amendChange(r.getChangeId());
}
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).current().submit();
assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(MERGED);
}
@Test
public void submitNotAllowedWithoutPermission() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(
AuthException.class,
() -> gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit());
assertThat(thrown).hasMessageThat().contains("submit not permitted");
}
@Test
public void submitAllowedWithPermission() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.SUBMIT).ref("refs/heads/master").group(REGISTERED_USERS))
.update();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(MERGED);
}
@Test
public void submitToSymref() throws Exception {
// Create symref in the origin repository (testRepo references to a local repository)
try (Repository repo = repoManager.openRepository(project)) {
RefUpdate u = repo.updateRef("refs/heads/master_symref");
assertThat(u.link("refs/heads/master")).isEqualTo(Result.NEW);
}
PushOneCommit.Result r = createChange("refs/for/master_symref");
String id = r.getChangeId();
gApi.changes().id(id).current().review(ReviewInput.approve());
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class, () -> gApi.changes().id(id).current().submit());
assertThat(thrown).hasMessageThat().contains("the target branch is a symbolic ref");
}
@Test
public void check() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(gApi.changes().id(r.getChangeId()).get().problems).isNull();
assertThat(gApi.changes().id(r.getChangeId()).get(CHECK).problems).isEmpty();
}
@Test
public void commitFooters() throws Exception {
LabelType verified =
label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
LabelType custom1 =
label("Custom1", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
LabelType custom2 =
label("Custom2", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().upsertLabelType(verified);
u.getConfig().upsertLabelType(custom1);
u.getConfig().upsertLabelType(custom2);
u.save();
}
projectOperations
.project(project)
.forUpdate()
.add(allowLabel(verified.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
.add(allowLabel(custom1.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
.add(allowLabel(custom2.getName()).ref("refs/heads/*").group(ANONYMOUS_USERS).range(-1, 1))
.update();
PushOneCommit.Result r1 = createChange();
r1.assertOkStatus();
PushOneCommit.Result r2 =
pushFactory
.create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId())
.to("refs/for/master");
r2.assertOkStatus();
ReviewInput in = new ReviewInput();
in.label(LabelId.CODE_REVIEW, 1);
in.label(LabelId.VERIFIED, 1);
in.label("Custom1", -1);
in.label("Custom2", 1);
gApi.changes().id(r2.getChangeId()).current().review(in);
ChangeInfo actual = gApi.changes().id(r2.getChangeId()).get(ALL_REVISIONS, COMMIT_FOOTERS);
assertThat(actual.revisions).hasSize(2);
// No footers except on latest patch set.
assertThat(actual.revisions.get(r1.getCommit().getName()).commitWithFooters).isNull();
List<String> footers =
new ArrayList<>(
Arrays.asList(
actual.revisions.get(r2.getCommit().getName()).commitWithFooters.split("\\n")));
// remove subject + blank line
footers.remove(0);
footers.remove(0);
List<String> expectedFooters =
Arrays.asList(
"Change-Id: " + r2.getChangeId(),
"Reviewed-on: "
+ canonicalWebUrl.get()
+ "c/"
+ project.get()
+ "/+/"
+ r2.getChange().getId(),
"Reviewed-by: Administrator <admin@example.com>",
"Custom2: Administrator <admin@example.com>",
"Tested-by: Administrator <admin@example.com>");
assertThat(footers).containsExactlyElementsIn(expectedFooters);
}
@Test
public void customCommitFooters() throws Exception {
PushOneCommit.Result change = createChange();
ChangeInfo actual;
ChangeMessageModifier link =
new ChangeMessageModifier() {
@Override
public String onSubmit(
String newCommitMessage,
RevCommit original,
RevCommit mergeTip,
BranchNameKey destination) {
assertThat(original.getName()).isNotEqualTo(mergeTip.getName());
return newCommitMessage + "Custom: " + destination.branch();
}
};
try (Registration registration = extensionRegistry.newRegistration().add(link)) {
actual = gApi.changes().id(change.getChangeId()).get(ALL_REVISIONS, COMMIT_FOOTERS);
}
List<String> footers =
new ArrayList<>(
Arrays.asList(
actual.revisions.get(change.getCommit().getName()).commitWithFooters.split("\\n")));
// remove subject + blank line
footers.remove(0);
footers.remove(0);
List<String> expectedFooters =
Arrays.asList(
"Change-Id: " + change.getChangeId(),
"Reviewed-on: "
+ canonicalWebUrl.get()
+ "c/"
+ project.get()
+ "/+/"
+ change.getChange().getId(),
"Custom: refs/heads/master");
assertThat(footers).containsExactlyElementsIn(expectedFooters);
}
@Test
public void defaultSearchDoesNotTouchDatabase() throws Exception {
requestScopeOperations.setApiUser(admin.id());
PushOneCommit.Result r1 = createChange();
gApi.changes()
.id(r1.getChangeId())
.revision(r1.getCommit().name())
.review(ReviewInput.approve());
gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
createChange();
requestScopeOperations.setApiUser(user.id());
try (AutoCloseable ignored = disableNoteDb()) {
assertThat(
gApi.changes()
.query()
.withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
.withOptions(IndexPreloadingUtil.DASHBOARD_OPTIONS)
.get())
.hasSize(2);
}
}
@Test
public void votable() throws Exception {
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(triplet).addReviewer(user.username());
ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
LabelInfo codeReview = c.labels.get(LabelId.CODE_REVIEW);
assertThat(codeReview.all).hasSize(1);
ApprovalInfo approval = codeReview.all.get(0);
assertThat(approval._accountId).isEqualTo(user.id().get());
assertThat(approval.value).isEqualTo(0);
projectOperations
.project(project)
.forUpdate()
.add(
blockLabel(LabelId.CODE_REVIEW)
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-1, 1))
.update();
c = gApi.changes().id(triplet).get(DETAILED_LABELS);
codeReview = c.labels.get(LabelId.CODE_REVIEW);
assertThat(codeReview.all).hasSize(1);
approval = codeReview.all.get(0);
assertThat(approval._accountId).isEqualTo(user.id().get());
assertThat(approval.value).isNull();
}
@Test
@GerritConfig(name = "gerrit.editGpgKeys", value = "true")
@GerritConfig(name = "receive.enableSignedPush", value = "true")
public void pushCertificates() throws Exception {
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = amendChange(r1.getChangeId());
ChangeInfo info = gApi.changes().id(r1.getChangeId()).get(ALL_REVISIONS, PUSH_CERTIFICATES);
RevisionInfo rev1 = info.revisions.get(r1.getCommit().name());
assertThat(rev1).isNotNull();
assertThat(rev1.pushCertificate).isNotNull();
assertThat(rev1.pushCertificate.certificate).isNull();
assertThat(rev1.pushCertificate.key).isNull();
RevisionInfo rev2 = info.revisions.get(r2.getCommit().name());
assertThat(rev2).isNotNull();
assertThat(rev2.pushCertificate).isNotNull();
assertThat(rev2.pushCertificate.certificate).isNull();
assertThat(rev2.pushCertificate.key).isNull();
}
@Test
public void anonymousRestApi() throws Exception {
requestScopeOperations.setApiUserAnonymous();
PushOneCommit.Result r = createChange();
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(info.changeId).isEqualTo(r.getChangeId());
String triplet = project.get() + "~master~" + r.getChangeId();
info = gApi.changes().id(triplet).get();
assertThat(info.changeId).isEqualTo(r.getChangeId());
info = gApi.changes().id(info._number).get();
assertThat(info.changeId).isEqualTo(r.getChangeId());
assertThrows(
AuthException.class,
() -> gApi.changes().id(triplet).current().review(ReviewInput.approve()));
}
@Test
public void commitsOnPatchSetCreation() throws Exception {
PushOneCommit.Result r = createChange();
pushFactory
.create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, "b.txt", "4711", r.getChangeId())
.to("refs/for/master")
.assertOkStatus();
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
try (Repository repo = repoManager.openRepository(project);
RevWalk rw = new RevWalk(repo)) {
RevCommit commitPatchSetCreation =
rw.parseCommit(repo.exactRef(changeMetaRef(Change.id(c._number))).getObjectId());
assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2");
PersonIdent expectedAuthor =
changeNoteUtil.newAccountIdIdent(
getAccount(admin.id()).id(), c.updated.toInstant(), serverIdent.get());
assertThat(commitPatchSetCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
assertThat(commitPatchSetCreation.getCommitterIdent())
.isEqualTo(new PersonIdent(serverIdent.get(), c.updated));
assertThat(commitPatchSetCreation.getParentCount()).isEqualTo(1);
RevCommit commitChangeCreation = rw.parseCommit(commitPatchSetCreation.getParent(0));
assertThat(commitChangeCreation.getShortMessage()).isEqualTo("Create change");
expectedAuthor =
changeNoteUtil.newAccountIdIdent(
getAccount(admin.id()).id(), c.created.toInstant(), serverIdent.get());
assertThat(commitChangeCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
assertThat(commitChangeCreation.getCommitterIdent())
.isEqualTo(new PersonIdent(serverIdent.get(), c.created));
assertThat(commitChangeCreation.getParentCount()).isEqualTo(0);
}
}
@Test
public void createEmptyChangeOnNonExistingBranch() throws Exception {
ChangeInput in = new ChangeInput();
in.branch = "foo";
in.subject = "Create a change on new branch from the API";
in.project = project.get();
in.newBranch = true;
ChangeInfo info = gApi.changes().create(in).get();
assertThat(info.project).isEqualTo(in.project);
assertThat(RefNames.fullName(info.branch)).isEqualTo(RefNames.fullName(in.branch));
assertThat(info.subject).isEqualTo(in.subject);
assertThat(Iterables.getOnlyElement(info.messages).message).isEqualTo("Uploaded patch set 1.");
}
@Test
public void createEmptyChangeOnExistingBranchWithNewBranch() throws Exception {
ChangeInput in = new ChangeInput();
in.branch = Constants.MASTER;
in.subject = "Create a change on new branch from the API";
in.project = project.get();
in.newBranch = true;
assertThrows(ResourceConflictException.class, () -> gApi.changes().create(in).get());
}
@Test
public void createNewPatchSetWithoutPermission() throws Exception {
// Create new project with clean permissions
Project.NameKey p = projectOperations.newProject().create();
// Clone separate repositories of the same project as admin and as user
TestRepository<InMemoryRepository> adminTestRepo = cloneProject(p, admin);
TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
// Block default permission
projectOperations
.project(p)
.forUpdate()
.add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
.update();
// Create change as admin
PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
PushOneCommit.Result r1 = push.to("refs/for/master");
r1.assertOkStatus();
// Fetch change
GitUtil.fetch(userTestRepo, r1.getPatchSet().refName() + ":ps");
userTestRepo.reset("ps");
// Amend change as user
PushOneCommit.Result r2 = amendChange(r1.getChangeId(), "refs/for/master", user, userTestRepo);
r2.assertErrorStatus("cannot add patch set to " + r1.getChange().getId().get() + ".");
}
@Test
public void createNewSetPatchWithPermission() throws Exception {
// Clone separate repositories of the same project as admin and as user
TestRepository<?> adminTestRepo = cloneProject(project, admin);
TestRepository<?> userTestRepo = cloneProject(project, user);
// Create change as admin
PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
PushOneCommit.Result r1 = push.to("refs/for/master");
r1.assertOkStatus();
// Fetch change
GitUtil.fetch(userTestRepo, r1.getPatchSet().refName() + ":ps");
userTestRepo.reset("ps");
// Amend change as user
PushOneCommit.Result r2 = amendChange(r1.getChangeId(), "refs/for/master", user, userTestRepo);
r2.assertOkStatus();
}
@Test
public void createNewPatchSetAsOwnerWithoutPermission() throws Exception {
// Create new project with clean permissions
Project.NameKey p = projectOperations.newProject().create();
// Clone separate repositories of the same project as admin and as user
TestRepository<?> adminTestRepo = cloneProject(project, admin);
// Block default permission
projectOperations
.project(p)
.forUpdate()
.add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
.update();
// Create change as admin
PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
PushOneCommit.Result r1 = push.to("refs/for/master");
r1.assertOkStatus();
// Fetch change
GitUtil.fetch(adminTestRepo, r1.getPatchSet().refName() + ":ps");
adminTestRepo.reset("ps");
// Amend change as admin
PushOneCommit.Result r2 =
amendChange(r1.getChangeId(), "refs/for/master", admin, adminTestRepo);
r2.assertOkStatus();
}
private void assertLabelDescription(ChangeInfo changeInfo, String labelName, String description) {
assertThat(changeInfo.labels.get(labelName).description).isEqualTo(description);
}
@Test
public void checkLabelVotesForUnsubmittedChange() throws Exception {
List<ListChangesOption> options =
EnumSet.complementOf(
EnumSet.of(
ListChangesOption.CHECK,
ListChangesOption.SKIP_DIFFSTAT,
ListChangesOption.DETAILED_LABELS))
.stream()
.collect(Collectors.toList());
PushOneCommit.Result r = createChange();
ChangeInfo change = gApi.changes().id(r.getChangeId()).get(options);
assertThat(change.status).isEqualTo(ChangeStatus.NEW);
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.labels.get(LabelId.CODE_REVIEW).all).isNull();
voteLabel(r.getChangeId(), LabelId.CODE_REVIEW, +1);
change = gApi.changes().id(r.getChangeId()).get(options);
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
List<ApprovalInfo> codeReviewApprovals = change.labels.get(LabelId.CODE_REVIEW).all;
assertThat(codeReviewApprovals).hasSize(1);
ApprovalInfo codeReviewApproval = codeReviewApprovals.get(0);
// permittedVotingRange is not served if DETAILED_LABELS is not requested.
assertThat(codeReviewApproval.permittedVotingRange).isNull();
assertThat(codeReviewApproval.value).isEqualTo(1);
assertThat(codeReviewApproval.username).isEqualTo(admin.username());
// Add another +1 vote as user
requestScopeOperations.setApiUser(user.id());
voteLabel(r.getChangeId(), LabelId.CODE_REVIEW, +1);
change = gApi.changes().id(r.getChangeId()).get(options);
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.labels.get(LabelId.CODE_REVIEW).all).hasSize(2);
// All available label votes and their meanings are also served if DETAILED_LABELS is not
// requested.
assertThat(change.labels.get(LabelId.CODE_REVIEW).values).isNotNull();
codeReviewApprovals = change.labels.get(LabelId.CODE_REVIEW).all;
assertThat(codeReviewApprovals.stream().map(a -> a.permittedVotingRange).collect(toList()))
.containsExactly(null, null);
assertThat(codeReviewApprovals.stream().map(a -> a.value).collect(toList()))
.containsExactly(1, 1);
assertThat(codeReviewApprovals.stream().map(a -> a.username).collect(toList()))
.containsExactly(admin.username(), user.username());
}
@Test
public void checkLabelsForUnsubmittedChange() throws Exception {
PushOneCommit.Result r = createChange();
ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.status).isEqualTo(ChangeStatus.NEW);
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
// add new label and assert that it's returned for existing changes
AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
LabelType verified = TestLabels.verified();
String heads = RefNames.REFS_HEADS + "*";
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().upsertLabelType(verified);
u.save();
}
projectOperations
.project(project)
.forUpdate()
.add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1))
.update();
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertThat(change.permittedLabels.keySet())
.containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertPermitted(change, LabelId.CODE_REVIEW, -2, -1, 0, 1, 2);
assertPermitted(change, LabelId.VERIFIED, -1, 0, 1);
assertLabelDescription(change, LabelId.VERIFIED, TestLabels.VERIFIED_LABEL_DESCRIPTION);
// add an approval on the new label
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(new ReviewInput().label(verified.getName(), verified.getMax().getValue()));
try (ProjectConfigUpdate u = updateProject(project)) {
// remove label and assert that it's no longer returned for existing
// changes, even if there is an approval for it
u.getConfig().getLabelSections().remove(verified.getName());
u.save();
}
projectOperations
.project(project)
.forUpdate()
.remove(
permissionKey(Permission.forLabel(verified.getName()))
.ref(heads)
.group(registeredUsers))
.update();
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
// abandon the change and see that the returned labels stay the same
// while all permitted labels disappear.
gApi.changes().id(r.getChangeId()).abandon();
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.status).isEqualTo(ChangeStatus.ABANDONED);
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels).isEmpty();
}
@Test
public void checkLabelVotesForMergedChange() throws Exception {
List<ListChangesOption> options =
EnumSet.complementOf(
EnumSet.of(
ListChangesOption.CHECK,
ListChangesOption.SKIP_DIFFSTAT,
ListChangesOption.DETAILED_LABELS))
.stream()
.collect(Collectors.toList());
PushOneCommit.Result r = createChange();
voteLabel(r.getChangeId(), LabelId.CODE_REVIEW, +2);
// Add another label for 'Verified'
LabelType verified = TestLabels.verified();
AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
String heads = RefNames.REFS_HEADS + "*";
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().upsertLabelType(verified);
u.save();
}
projectOperations
.project(project)
.forUpdate()
.add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1))
.update();
// Submit the change
voteLabel(r.getChangeId(), TestLabels.verified().getName(), 1);
gApi.changes().id(r.getChangeId()).current().submit();
// Make sure label votes are available if DETAILED_LABELS is not requested.
ChangeInfo change = gApi.changes().id(r.getChangeId()).get(options);
assertThat(change.status).isEqualTo(ChangeStatus.MERGED);
assertThat(change.labels.keySet())
.containsExactly(LabelId.CODE_REVIEW, TestLabels.verified().getName());
List<ApprovalInfo> codeReviewApprovals = change.labels.get(LabelId.CODE_REVIEW).all;
List<ApprovalInfo> verifiedApprovals = change.labels.get(TestLabels.verified().getName()).all;
assertThat(codeReviewApprovals).hasSize(1);
assertThat(codeReviewApprovals.get(0).value).isEqualTo(2);
assertThat(codeReviewApprovals.get(0).username).isEqualTo(admin.username());
assertThat(codeReviewApprovals.get(0).permittedVotingRange).isNull();
assertThat(verifiedApprovals).hasSize(1);
assertThat(verifiedApprovals.get(0).value).isEqualTo(1);
assertThat(verifiedApprovals.get(0).username).isEqualTo(admin.username());
assertThat(codeReviewApprovals.get(0).permittedVotingRange).isNull();
}
@Test
public void checkLabelsForMergedChange() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.status).isEqualTo(MERGED);
assertThat(change.submissionId).isNotNull();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
LabelType verified = TestLabels.verified();
AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
String heads = RefNames.REFS_HEADS + "*";
// add new label and assert that it's returned for existing changes
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().upsertLabelType(verified);
u.save();
}
projectOperations
.project(project)
.forUpdate()
.add(allowLabel(verified.getName()).ref(heads).group(registeredUsers).range(-1, 1))
.update();
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertThat(change.permittedLabels.keySet())
.containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
assertPermitted(change, LabelId.VERIFIED, 0, 1);
assertLabelDescription(change, LabelId.VERIFIED, TestLabels.VERIFIED_LABEL_DESCRIPTION);
// Ignore the new label by Prolog submit rule. Permitted ranges are still going to be
// returned for the label.
GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
testRepo.reset("config");
PushOneCommit push2 =
pushFactory.create(
admin.newIdent(),
testRepo,
"Ignore Verified",
"rules.pl",
"submit_rule(submit(CR)) :-\n gerrit:max_with_block(-2, 2, 'Code-Review', CR).");
push2.to(RefNames.REFS_CONFIG);
change = gApi.changes().id(r.getChangeId()).get();
assertPermitted(change, LabelId.CODE_REVIEW, 2);
assertPermitted(change, LabelId.VERIFIED, 0, 1);
// add an approval on the new label. The label can still be voted +1 although it is ignored
// in Prolog. 0 is not permitted because votes cannot be decreased.
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(new ReviewInput().label(verified.getName(), verified.getMax().getValue()));
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
assertPermitted(change, LabelId.VERIFIED, 1);
// remove label and assert that it's no longer returned for existing
// changes, even if there is an approval for it
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().getLabelSections().remove(verified.getName());
u.save();
}
projectOperations
.project(project)
.forUpdate()
.remove(permissionKey(verified.getName()).ref(heads).group(registeredUsers))
.update();
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
}
@Test
public void notifyConfigForDirectoryTriggersEmail() throws Exception {
// Configure notifications on project level.
RevCommit oldHead = projectOperations.project(project).getHead("master");
GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
testRepo.reset("config");
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
"Configure Notifications",
"project.config",
"[notify \"my=notify-config\"]\n"
+ " email = foo@example.com\n"
+ " filter = dir:\\\"foo/bar/baz\\\"");
push.to(RefNames.REFS_CONFIG);
testRepo.reset(oldHead);
// Push a change that matches the filter.
sender.clear();
push =
pushFactory.create(
admin.newIdent(), testRepo, "Test change", "foo/bar/baz/test.txt", "some content");
PushOneCommit.Result r = push.to("refs/for/master");
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).rcpt())
.containsExactly(Address.parse("foo@example.com"));
// Comment on the change.
sender.clear();
ReviewInput reviewInput = new ReviewInput();
reviewInput.message = "some message";
gApi.changes().id(r.getChangeId()).current().review(reviewInput);
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).rcpt())
.containsExactly(Address.parse("foo@example.com"));
}
@Test
public void checkLabelsForMergedChangeWithNonAuthorCodeReview() throws Exception {
// Configure Non-Author-Code-Review
RevCommit oldHead = projectOperations.project(project).getHead("master");
GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config");
testRepo.reset("config");
PushOneCommit push2 =
pushFactory.create(
admin.newIdent(),
testRepo,
"Configure Non-Author-Code-Review",
"rules.pl",
"submit_rule(S) :-\n"
+ " gerrit:default_submit(X),\n"
+ " X =.. [submit | Ls],\n"
+ " add_non_author_approval(Ls, R),\n"
+ " S =.. [submit | R].\n"
+ "\n"
+ "add_non_author_approval(S1, S2) :-\n"
+ " gerrit:commit_author(A),\n"
+ " gerrit:commit_label(label('Code-Review', 2), R),\n"
+ " R \\= A, !,\n"
+ " S2 = [label('Non-Author-Code-Review', ok(R)) | S1].\n"
+ "add_non_author_approval(S1,"
+ " [label('Non-Author-Code-Review', need(_)) | S1]).");
push2.to(RefNames.REFS_CONFIG);
testRepo.reset(oldHead);
String heads = RefNames.REFS_HEADS + "*";
// Allow user to approve
projectOperations
.project(project)
.forUpdate()
.add(
allowLabel(TestLabels.codeReview().getName())
.ref(heads)
.group(REGISTERED_USERS)
.range(-2, 2))
.update();
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.status).isEqualTo(MERGED);
assertThat(change.submissionId).isNotNull();
assertThat(change.labels.keySet())
.containsExactly(LabelId.CODE_REVIEW, "Non-Author-Code-Review");
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 0, 1, 2);
}
@Test
public void checkLabelsForAutoClosedChange() throws Exception {
PushOneCommit.Result r = createChange();
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result result = push.to("refs/heads/master");
result.assertOkStatus();
ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.status).isEqualTo(MERGED);
assertThat(change.submissionId).isNotNull();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 0, 1, 2);
}
@Test
public void checkSubmissionIdForAutoClosedChange() throws Exception {
PushOneCommit.Result first = createChange();
PushOneCommit.Result second = createChange();
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result result = push.to("refs/heads/master");
result.assertOkStatus();
ChangeInfo firstChange = gApi.changes().id(first.getChangeId()).get();
assertThat(firstChange.status).isEqualTo(MERGED);
assertThat(firstChange.submissionId).isNotNull();
ChangeInfo secondChange = gApi.changes().id(second.getChangeId()).get();
assertThat(secondChange.status).isEqualTo(MERGED);
assertThat(secondChange.submissionId).isNotNull();
assertThat(secondChange.submissionId).isEqualTo(firstChange.submissionId);
assertThat(gApi.changes().id(second.getChangeId()).submittedTogether()).hasSize(2);
}
@Test
public void maxPermittedValueAllowed() throws Exception {
final int minPermittedValue = -2;
final int maxPermittedValue = +2;
String heads = "refs/heads/*";
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(triplet).addReviewer(user.username());
ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
LabelInfo codeReview = c.labels.get(LabelId.CODE_REVIEW);
assertThat(codeReview.all).hasSize(1);
ApprovalInfo approval = codeReview.all.get(0);
assertThat(approval._accountId).isEqualTo(user.id().get());
assertThat(approval.permittedVotingRange).isNotNull();
// default values
assertThat(approval.permittedVotingRange.min).isEqualTo(-1);
assertThat(approval.permittedVotingRange.max).isEqualTo(1);
projectOperations
.project(project)
.forUpdate()
.add(
allowLabel(LabelId.CODE_REVIEW)
.ref(heads)
.group(REGISTERED_USERS)
.range(minPermittedValue, maxPermittedValue))
.update();
c = gApi.changes().id(triplet).get(DETAILED_LABELS);
codeReview = c.labels.get(LabelId.CODE_REVIEW);
assertThat(codeReview.all).hasSize(1);
approval = codeReview.all.get(0);
assertThat(approval._accountId).isEqualTo(user.id().get());
assertThat(approval.permittedVotingRange).isNotNull();
assertThat(approval.permittedVotingRange.min).isEqualTo(minPermittedValue);
assertThat(approval.permittedVotingRange.max).isEqualTo(maxPermittedValue);
}
@Test
public void maxPermittedValueBlocked() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(
blockLabel(LabelId.CODE_REVIEW)
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-1, 1))
.update();
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(triplet).addReviewer(user.username());
ChangeInfo c = gApi.changes().id(triplet).get(DETAILED_LABELS);
LabelInfo codeReview = c.labels.get(LabelId.CODE_REVIEW);
assertThat(codeReview.all).hasSize(1);
ApprovalInfo approval = codeReview.all.get(0);
assertThat(approval._accountId).isEqualTo(user.id().get());
assertThat(approval.permittedVotingRange).isNull();
}
@Test
public void nonStrictLabelWithInvalidLabelPerDefault() throws Exception {
String changeId = createChange().getChangeId();
// Add a review with invalid labels.
ReviewInput input = ReviewInput.approve().label("Code-Style", 1);
gApi.changes().id(changeId).current().review(input);
Map<String, Short> votes =
gApi.changes().id(changeId).current().reviewer(admin.email()).votes();
assertThat(votes.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(votes.values()).containsExactly((short) 2);
}
@Test
public void nonStrictLabelWithInvalidValuePerDefault() throws Exception {
String changeId = createChange().getChangeId();
// Add a review with invalid label values.
ReviewInput input = new ReviewInput().label(LabelId.CODE_REVIEW, 3);
gApi.changes().id(changeId).current().review(input);
assertThrows(
ResourceNotFoundException.class,
() -> gApi.changes().id(changeId).current().reviewer(admin.email()));
}
@Test
@GerritConfig(name = "change.strictLabels", value = "true")
public void strictLabelWithInvalidLabel() throws Exception {
String changeId = createChange().getChangeId();
ReviewInput in = new ReviewInput().label("Code-Style", 1);
BadRequestException thrown =
assertThrows(
BadRequestException.class, () -> gApi.changes().id(changeId).current().review(in));
assertThat(thrown).hasMessageThat().contains("label \"Code-Style\" is not a configured label");
}
@Test
@GerritConfig(name = "change.strictLabels", value = "true")
public void strictLabelWithInvalidValue() throws Exception {
String changeId = createChange().getChangeId();
ReviewInput in = new ReviewInput().label(LabelId.CODE_REVIEW, 3);
BadRequestException thrown =
assertThrows(
BadRequestException.class, () -> gApi.changes().id(changeId).current().review(in));
assertThat(thrown).hasMessageThat().contains("label \"Code-Review\": 3 is not a valid value");
}
@Test
public void unresolvedCommentsBlocked() throws Exception {
modifySubmitRules(
"submit_rule(submit(R)) :- \n"
+ "gerrit:unresolved_comments_count(0), \n"
+ "!,"
+ "gerrit:uploader(U), \n"
+ "R = label('All-Comments-Resolved', ok(U)).\n"
+ "submit_rule(submit(R)) :- \n"
+ "gerrit:unresolved_comments_count(U), \n"
+ "U > 0,"
+ "R = label('All-Comments-Resolved', need(_)). \n\n");
String oldHead = projectOperations.project(project).getHead("master").name();
PushOneCommit.Result result1 =
pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
testRepo.reset(oldHead);
PushOneCommit.Result result2 =
pushFactory.create(user.newIdent(), testRepo).to("refs/for/master");
addComment(result1, "comment 1", true, false, null);
addComment(result2, "comment 2", true, true, null);
gApi.changes().id(result1.getChangeId()).current().submit();
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(result2.getChangeId()).current().submit());
assertThat(thrown)
.hasMessageThat()
.contains("Failed to submit 1 change due to the following problems");
assertThat(thrown)
.hasMessageThat()
.contains("submit requirement 'All-Comments-Resolved' is unsatisfied");
}
@Test
public void changeCommitMessage() throws Exception {
// Tests mutating the commit message as both the owner of the change and a regular user with
// addPatchSet permission. Asserts that both cases succeed.
PushOneCommit.Result r = createChange();
r.assertOkStatus();
assertThat(getCommitMessage(r.getChangeId()))
.isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
for (com.google.gerrit.acceptance.TestAccount acc : ImmutableList.of(admin, user)) {
requestScopeOperations.setApiUser(acc.id());
String newMessage =
"modified commit by " + acc.id() + "\n\nChange-Id: " + r.getChangeId() + "\n";
gApi.changes().id(r.getChangeId()).setMessage(newMessage);
RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
assertThat(rApi.description()).isEqualTo("Edit commit message");
}
// Verify tags, which should differ according to whether the change was WIP
// at the time the commit message was edited. First, look at the last edit
// we created above, when the change was not WIP.
ChangeInfo info = gApi.changes().id(r.getChangeId()).get();
assertThat(Iterables.getLast(info.messages).tag)
.isEqualTo(ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET);
// Move the change to WIP and edit the commit message again, to observe a
// different tag. Must switch to change owner to move into WIP.
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(r.getChangeId()).setWorkInProgress();
String newMessage = "modified commit in WIP change\n\nChange-Id: " + r.getChangeId() + "\n";
gApi.changes().id(r.getChangeId()).setMessage(newMessage);
info = gApi.changes().id(r.getChangeId()).get();
assertThat(Iterables.getLast(info.messages).tag)
.isEqualTo(ChangeMessagesUtil.TAG_UPLOADED_WIP_PATCH_SET);
}
@Test
public void changeCommitMessageWithNoChangeIdSucceedsIfChangeIdNotRequired() throws Exception {
ConfigInput configInput = new ConfigInput();
configInput.requireChangeId = InheritableBoolean.FALSE;
gApi.projects().name(project.get()).config(configInput);
PushOneCommit.Result r = createChange();
r.assertOkStatus();
assertThat(getCommitMessage(r.getChangeId()))
.isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
String newMessage = "modified commit\n";
gApi.changes().id(r.getChangeId()).setMessage(newMessage);
RevisionApi rApi = gApi.changes().id(r.getChangeId()).current();
assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt");
assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage);
}
@Test
public void changeCommitMessageNullNotAllowed() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(getCommitMessage(r.getChangeId()))
.isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
BadRequestException thrown =
assertThrows(
BadRequestException.class,
() ->
gApi.changes()
.id(r.getChangeId())
.setMessage("test\0commit\n\nChange-Id: " + r.getChangeId() + "\n"));
assertThat(thrown).hasMessageThat().contains("NUL character");
}
@Test
public void changeCommitMessageWithWrongChangeIdFails() throws Exception {
PushOneCommit.Result otherChange = createChange();
PushOneCommit.Result r = createChange();
assertThat(getCommitMessage(r.getChangeId()))
.isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() ->
gApi.changes()
.id(r.getChangeId())
.setMessage(
"modified commit\n\nChange-Id: " + otherChange.getChangeId() + "\n"));
assertThat(thrown).hasMessageThat().contains("wrong Change-Id footer");
}
@Test
public void changeCommitMessageWithoutPermissionFails() throws Exception {
// Create new project with clean permissions
Project.NameKey p = projectOperations.newProject().create();
TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user);
// Block default permission
projectOperations
.project(p)
.forUpdate()
.add(block(Permission.ADD_PATCH_SET).ref("refs/for/*").group(REGISTERED_USERS))
.update();
// Create change as user
PushOneCommit push = pushFactory.create(user.newIdent(), userTestRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
// Try to change the commit message
AuthException thrown =
assertThrows(
AuthException.class, () -> gApi.changes().id(r.getChangeId()).setMessage("foo"));
assertThat(thrown).hasMessageThat().contains("modifying commit message not permitted");
}
@Test
public void changeCommitMessageWithSameMessageFails() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(getCommitMessage(r.getChangeId()))
.isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class,
() -> gApi.changes().id(r.getChangeId()).setMessage(getCommitMessage(r.getChangeId())));
assertThat(thrown).hasMessageThat().contains("new and existing commit message are the same");
}
@Test
public void fourByteEmoji() throws Exception {
// U+1F601 GRINNING FACE WITH SMILING EYES
String smile = new String(Character.toChars(0x1f601));
assertThat(smile).isEqualTo("😁");
assertThat(smile).hasLength(2); // Thanks, Java.
assertThat(smile.getBytes(UTF_8)).hasLength(4);
String subject = "A happy change " + smile;
PushOneCommit.Result r =
pushFactory
.create(admin.newIdent(), testRepo, subject, FILE_NAME, FILE_CONTENT)
.to("refs/for/master");
r.assertOkStatus();
String id = r.getChangeId();
ReviewInput ri = ReviewInput.approve();
ri.message = "I like it " + smile;
ReviewInput.CommentInput ci = new ReviewInput.CommentInput();
ci.path = FILE_NAME;
ci.side = Side.REVISION;
ci.message = "Good " + smile;
ri.comments = ImmutableMap.of(FILE_NAME, ImmutableList.of(ci));
gApi.changes().id(id).current().review(ri);
ChangeInfo info = gApi.changes().id(id).get(MESSAGES, CURRENT_COMMIT, CURRENT_REVISION);
assertThat(info.subject).isEqualTo(subject);
assertThat(Iterables.getLast(info.messages).message).endsWith(ri.message);
assertThat(Iterables.getOnlyElement(info.revisions.values()).commit.message)
.startsWith(subject);
List<CommentInfo> comments =
Iterables.getOnlyElement(gApi.changes().id(id).commentsRequest().get().values());
assertThat(Iterables.getOnlyElement(comments).message).isEqualTo(ci.message);
}
@Test
public void putTopicExceedLimitFails() throws Exception {
String changeId = createChange().getChangeId();
String topic = Stream.generate(() -> "t").limit(2049).collect(joining());
BadRequestException thrown =
assertThrows(BadRequestException.class, () -> gApi.changes().id(changeId).topic(topic));
assertThat(thrown).hasMessageThat().contains("topic length exceeds the limit");
}
@Test
public void submittableAfterLosingPermissions_MaxWithBlock() throws Exception {
configLabel("Label", LabelFunction.MAX_WITH_BLOCK);
submittableAfterLosingPermissions("Label");
}
@Test
public void submittableAfterLosingPermissions_AnyWithBlock() throws Exception {
configLabel("Label", LabelFunction.ANY_WITH_BLOCK);
submittableAfterLosingPermissions("Label");
}
@Test
@GerritConfig(name = "change.submitWholeTopic", value = "true")
public void cantSubmitWithInvisibleChangesWithTopic() throws Exception {
createBranch(BranchNameKey.create(project, "secret"));
// create two changes in the same topic.
String topic = "topic";
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange("refs/for/secret");
approve(r1.getChangeId());
approve(r2.getChangeId());
gApi.changes().id(r1.getChangeId()).topic(topic);
gApi.changes().id(r2.getChangeId()).topic(topic);
// make one of the changes invisible.
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.READ).ref("refs/heads/secret").group(REGISTERED_USERS))
.update();
// can't submit with invisible changes.
requestScopeOperations.setApiUser(user.id());
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
.update();
assertThrows(AuthException.class, () -> gApi.changes().id(r1.getChangeId()).current().submit());
}
@Test
public void cantSubmitWithInvisibleDependentChange() throws Exception {
// create two dependent changes.
PushOneCommit.Result r1 = createChange();
PushOneCommit.Result r2 = createChange();
approve(r1.getChangeId());
approve(r2.getChangeId());
// make the dependent change invisible.
gApi.changes().id(r1.getChangeId()).setPrivate(true);
// can't submit with invisible changes.
requestScopeOperations.setApiUser(user.id());
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.SUBMIT).ref("refs/*").group(REGISTERED_USERS))
.update();
assertThrows(AuthException.class, () -> gApi.changes().id(r2.getChangeId()).current().submit());
}
private void submittableAfterLosingPermissions(String label) throws Exception {
String codeReviewLabel = LabelId.CODE_REVIEW;
AccountGroup.UUID registered = REGISTERED_USERS;
projectOperations
.project(project)
.forUpdate()
.add(allowLabel(label).ref("refs/heads/*").group(registered).range(-1, +1))
.add(allowLabel(codeReviewLabel).ref("refs/heads/*").group(registered).range(-2, +2))
.update();
requestScopeOperations.setApiUser(user.id());
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
// Verify user's permitted range.
ChangeInfo change = gApi.changes().id(changeId).get();
assertPermitted(change, label, -1, 0, 1);
assertPermitted(change, codeReviewLabel, -2, -1, 0, 1, 2);
ReviewInput input = new ReviewInput();
input.label(codeReviewLabel, 2);
input.label(label, 1);
gApi.changes().id(changeId).current().review(input);
assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().keySet())
.containsExactly(codeReviewLabel, label);
assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().values())
.containsExactly((short) 2, (short) 1);
assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
requestScopeOperations.setApiUser(admin.id());
projectOperations
.project(project)
.forUpdate()
.remove(labelPermissionKey(label).ref("refs/heads/*").group(registered))
.remove(labelPermissionKey(codeReviewLabel).ref("refs/heads/*").group(registered))
.add(allowLabel(codeReviewLabel).ref("refs/heads/*").group(registered).range(-1, +1))
.update();
// Verify user's new permitted range.
requestScopeOperations.setApiUser(user.id());
change = gApi.changes().id(changeId).get();
assertPermitted(change, label);
assertPermitted(change, codeReviewLabel, -1, 0, 1);
assertThat(gApi.changes().id(changeId).current().reviewer(user.email()).votes().values())
.containsExactly((short) 2, (short) 1);
assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
requestScopeOperations.setApiUser(admin.id());
gApi.changes().id(changeId).current().submit();
}
@Test
public void draftCommentsShouldNotUpdateChangeTimestamp() throws Exception {
String changeId = createNewChange();
Timestamp changeTs = getChangeLastUpdate(changeId);
DraftApi draftApi = addDraftComment(changeId);
assertThat(getChangeLastUpdate(changeId)).isEqualTo(changeTs);
draftApi.delete();
assertThat(getChangeLastUpdate(changeId)).isEqualTo(changeTs);
}
@Test
public void deletingAllDraftCommentsShouldNotUpdateChangeTimestamp() throws Exception {
String changeId = createNewChange();
Timestamp changeTs = getChangeLastUpdate(changeId);
addDraftComment(changeId);
assertThat(getChangeLastUpdate(changeId)).isEqualTo(changeTs);
gApi.accounts().self().deleteDraftComments(new DeleteDraftCommentsInput());
assertThat(getChangeLastUpdate(changeId)).isEqualTo(changeTs);
}
private Timestamp getChangeLastUpdate(String changeId) throws RestApiException {
Timestamp changeTs = gApi.changes().id(changeId).get().updated;
return changeTs;
}
private String createNewChange() throws Exception {
TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
PushOneCommit.Result result =
pushFactory.create(user.newIdent(), userRepo).to("refs/for/master");
String changeId = result.getChangeId();
return changeId;
}
private DraftApi addDraftComment(String changeId) throws RestApiException {
DraftInput comment = new DraftInput();
comment.message = "foo";
comment.path = "/foo";
return gApi.changes().id(changeId).current().createDraft(comment);
}
private String getCommitMessage(String changeId) throws RestApiException, IOException {
return gApi.changes().id(changeId).current().file("/COMMIT_MSG").content().asString();
}
private void addComment(
PushOneCommit.Result r,
String message,
boolean omitDuplicateComments,
Boolean unresolved,
String inReplyTo)
throws Exception {
ReviewInput.CommentInput c = new ReviewInput.CommentInput();
c.line = 1;
c.message = message;
c.path = FILE_NAME;
c.unresolved = unresolved;
c.inReplyTo = inReplyTo;
ReviewInput in = new ReviewInput();
in.comments = new HashMap<>();
in.comments.put(c.path, Lists.newArrayList(c));
in.omitDuplicateComments = omitDuplicateComments;
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
}
private static Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
if (r == null) {
return ImmutableList.of();
}
return Iterables.transform(r, a -> Account.id(a._accountId));
}
private ChangeResource parseResource(PushOneCommit.Result r) throws Exception {
return parseChangeResource(r.getChangeId());
}
private Optional<ReviewerState> getReviewerState(String changeId, Account.Id accountId)
throws Exception {
ChangeInfo c = gApi.changes().id(changeId).get(DETAILED_LABELS);
Set<ReviewerState> states =
c.reviewers.entrySet().stream()
.filter(e -> e.getValue().stream().anyMatch(a -> a._accountId == accountId.get()))
.map(Map.Entry::getKey)
.collect(toSet());
assertWithMessage(states.toString()).that(states.size()).isAtMost(1);
return states.stream().findFirst();
}
private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception {
try (BatchUpdate batchUpdate =
batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.now())) {
batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus));
batchUpdate.execute();
}
ChangeStatus changeStatus = gApi.changes().id(id.get()).get().status;
assertThat(changeStatus).isEqualTo(newStatus.asChangeStatus());
}
private static class ChangeStatusUpdateOp implements BatchUpdateOp {
private final Change.Status newStatus;
ChangeStatusUpdateOp(Change.Status newStatus) {
this.newStatus = newStatus;
}
@Override
public boolean updateChange(ChangeContext ctx) throws Exception {
Change change = ctx.getChange();
// Change status.
PatchSet.Id currentPatchSetId = change.currentPatchSetId();
ctx.getUpdate(currentPatchSetId).setStatus(newStatus);
return true;
}
}
private void modifySubmitRules(String newContent) throws Exception {
try (Repository repo = repoManager.openRepository(project);
TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
testRepo
.branch(RefNames.REFS_CONFIG)
.commit()
.author(admin.newIdent())
.committer(admin.newIdent())
.add("rules.pl", newContent)
.message("Modify rules.pl")
.create();
}
projectCache.evict(project);
}
@Test
@GerritConfig(name = "trackingid.jira-bug.footer", value = "Bug:")
@GerritConfig(name = "trackingid.jira-bug.match", value = "JRA\\d{2,8}")
@GerritConfig(name = "trackingid.jira-bug.system", value = "JIRA")
public void trackingIds() throws Exception {
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT + "\n\n" + "Bug:JRA001",
PushOneCommit.FILE_NAME,
PushOneCommit.FILE_CONTENT);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
ChangeInfo change = gApi.changes().id(result.getChangeId()).get(TRACKING_IDS);
Collection<TrackingIdInfo> trackingIds = change.trackingIds;
assertThat(trackingIds).isNotNull();
assertThat(trackingIds).hasSize(1);
assertThat(trackingIds.iterator().next().system).isEqualTo("JIRA");
assertThat(trackingIds.iterator().next().id).isEqualTo("JRA001");
}
@Test
@GerritConfig(name = "trackingid.jira-bug.footer", value = "Bug:")
@GerritConfig(name = "trackingid.jira-bug.match", value = "\\d+")
@GerritConfig(name = "trackingid.jira-bug.system", value = "JIRA")
public void multipleTrackingIdsInSingleFooter() throws Exception {
PushOneCommit push =
pushFactory.create(
admin.newIdent(),
testRepo,
PushOneCommit.SUBJECT + "\n\n" + "Bug: 123, 456",
PushOneCommit.FILE_NAME,
PushOneCommit.FILE_CONTENT);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
ChangeInfo change = gApi.changes().id(result.getChangeId()).get(TRACKING_IDS);
Collection<TrackingIdInfo> trackingIds = change.trackingIds;
assertThat(trackingIds).isNotNull();
assertThat(trackingIds).hasSize(2);
assertThat(trackingIds.stream().map(t -> t.id)).containsExactly("123", "456");
}
@Test
public void starUnstar() throws Exception {
ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(changeIndexedCounter)) {
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
changeIndexedCounter.clear();
gApi.accounts().self().starChange(triplet);
ChangeInfo change = info(triplet);
assertThat(change.starred).isTrue();
assertThat(change.stars).contains(DEFAULT_LABEL);
// change was not re-indexed
changeIndexedCounter.assertReindexOf(change, 0);
gApi.accounts().self().unstarChange(triplet);
change = info(triplet);
assertThat(change.starred).isNull();
assertThat(change.stars).isNull();
// change was not re-indexed
changeIndexedCounter.assertReindexOf(change, 0);
}
}
@Test
public void createAndDeleteDraftCommentDoesNotTriggerChangeReindex() throws Exception {
ChangeIndexedCounter changeIndexedCounter = new ChangeIndexedCounter();
try (Registration registration =
extensionRegistry.newRegistration().add(changeIndexedCounter)) {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
String revId = r.getCommit().getName();
String triplet = project.get() + "~master~" + r.getChangeId();
changeIndexedCounter.clear();
// Create the draft. Change is not re-indexed
DraftInput draftInput =
CommentsUtil.newDraft("file1", Side.REVISION, /* line= */ 1, "comment 1");
CommentInfo draftInfo =
gApi.changes().id(changeId).revision(revId).createDraft(draftInput).get();
ChangeInfo change = info(triplet);
changeIndexedCounter.assertReindexOf(change, 0);
// Delete the draft comment. Change is not re-indexed
gApi.changes().id(changeId).revision(revId).draft(draftInfo.id).delete();
changeIndexedCounter.assertReindexOf(change, 0);
}
}
@Test
public void changeDetailsDoesNotRequireIndex() throws Exception {
// This set of options must be kept in sync with gr-rest-api-interface.js
Set<ListChangesOption> options =
ImmutableSet.of(
ListChangesOption.ALL_COMMITS,
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CHANGE_ACTIONS,
ListChangesOption.DETAILED_LABELS,
ListChangesOption.DOWNLOAD_COMMANDS,
ListChangesOption.MESSAGES,
ListChangesOption.SUBMITTABLE,
ListChangesOption.WEB_LINKS,
ListChangesOption.SKIP_DIFFSTAT);
PushOneCommit.Result change = createChange();
int number = gApi.changes().id(change.getChangeId()).get()._number;
try (AutoCloseable ignored = changeIndexOperations.disableReadsAndWrites()) {
assertThat(gApi.changes().id(project.get(), number).get(options).changeId)
.isEqualTo(change.getChangeId());
}
}
@Test
@GerritConfig(
name = "change.mergeabilityComputationBehavior",
value = "API_REF_UPDATED_AND_CHANGE_REINDEX")
public void changeQueryReturnsMergeableWhenGerritIndexMergeable() throws Exception {
String changeId = createChange().getChangeId();
assertThat(gApi.changes().query(changeId).get().get(0).mergeable).isTrue();
}
@Test
@GerritConfig(name = "change.mergeabilityComputationBehavior", value = "NEVER")
public void changeQueryDoesNotReturnMergeableWhenGerritDoesNotIndexMergeable() throws Exception {
String changeId = createChange().getChangeId();
assertThat(gApi.changes().query(changeId).get().get(0).mergeable).isNull();
}
@Test
public void ccUserThatCannotSeeTheChange() throws Exception {
// Create a project that is only visible to admin users.
Project.NameKey project = projectOperations.newProject().create();
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.READ).ref("refs/*").group(adminGroupUuid()))
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
// Create a change
TestRepository<?> adminTestRepo = cloneProject(project, admin);
PushOneCommit push = pushFactory.create(admin.newIdent(), adminTestRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
// Check that the change is not visible to user.
requestScopeOperations.setApiUser(user.id());
assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(r.getChangeId()).get());
// Add user as a CC.
requestScopeOperations.setApiUser(admin.id());
ReviewerInput reviewerInput = new ReviewerInput();
reviewerInput.state = CC;
reviewerInput.reviewer = user.id().toString();
gApi.changes().id(r.getChangeId()).addReviewer(reviewerInput);
// Check that user was not added as a CC since they cannot see the change. Note,
// ChangeInfo#reviewers is a map that also contains CCs (if any are present).
assertThat(gApi.changes().id(r.getChangeId()).get().reviewers).isEmpty();
// Check that the change is still not visible to user.
requestScopeOperations.setApiUser(user.id());
assertThrows(ResourceNotFoundException.class, () -> gApi.changes().id(r.getChangeId()).get());
}
@Test
public void ccNonExistentAccountByEmailThenRemoveByDelete() throws Exception {
// Create a project that allows reviewers by email.
Project.NameKey project = projectOperations.newProject().create();
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig()
.updateProject(
b ->
b.setBooleanConfig(
BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.TRUE));
u.save();
}
// Create a change
TestRepository<?> testRepo = cloneProject(project, admin);
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
// Add an email as a CC for which no Gerrit account exists.
sender.clear();
ReviewerInput reviewerInput = new ReviewerInput();
reviewerInput.state = CC;
reviewerInput.reviewer = "email-without-account@example.com";
gApi.changes().id(r.getChangeId()).addReviewer(reviewerInput);
// Check that the email was added as a CC and an email was sent.
AccountInfo ccedAccountInfo =
Iterables.getOnlyElement(
gApi.changes().id(r.getChangeId()).get().reviewers.get(ReviewerState.CC));
assertThat(ccedAccountInfo.email).isEqualTo(reviewerInput.reviewer);
assertThat(ccedAccountInfo._accountId).isNull();
assertThat(ccedAccountInfo.name).isNull();
assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
.contains(String.format("%s has uploaded this change for review", admin.fullName()));
// Remove the CC.
sender.clear();
gApi.changes().id(r.getChangeId()).reviewer(reviewerInput.reviewer).remove();
// Check that the email was removed as a CC and an email was sent.
assertThat(gApi.changes().id(r.getChangeId()).get().reviewers).isEmpty();
assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
.contains(String.format("%s has removed %s", admin.fullName(), reviewerInput.reviewer));
}
@Test
public void ccNonExistentAccountByEmailThenRemoveByPostReview() throws Exception {
// Create a project that allows reviewers by email.
Project.NameKey project = projectOperations.newProject().create();
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig()
.updateProject(
b ->
b.setBooleanConfig(
BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.TRUE));
u.save();
}
// Create a change
TestRepository<?> testRepo = cloneProject(project, admin);
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
// Add an email as a CC for which no Gerrit account exists.
sender.clear();
ReviewerInput reviewerInput = new ReviewerInput();
reviewerInput.state = CC;
reviewerInput.reviewer = "email-without-account@example.com";
gApi.changes().id(r.getChangeId()).addReviewer(reviewerInput);
// Check that the email was added as a CC and an email was sent.
AccountInfo ccedAccountInfo =
Iterables.getOnlyElement(
gApi.changes().id(r.getChangeId()).get().reviewers.get(ReviewerState.CC));
assertThat(ccedAccountInfo.email).isEqualTo(reviewerInput.reviewer);
assertThat(ccedAccountInfo._accountId).isNull();
assertThat(ccedAccountInfo.name).isNull();
assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
.contains(String.format("%s has uploaded this change for review", admin.fullName()));
// Remove the CC.
sender.clear();
ReviewInput reviewInput = new ReviewInput();
reviewInput.reviewer(reviewerInput.reviewer, ReviewerState.REMOVED, /* confirmed= */ false);
gApi.changes().id(r.getChangeId()).current().review(reviewInput);
// Check that the email was removed as a CC and an email was sent.
assertThat(gApi.changes().id(r.getChangeId()).get().reviewers).isEmpty();
assertThat(Iterables.getOnlyElement(sender.getMessages()).body())
.contains(String.format("%s has removed %s", admin.fullName(), reviewerInput.reviewer));
}
private PushOneCommit.Result createWorkInProgressChange() throws Exception {
return pushTo("refs/for/master%wip");
}
private ThrowableSubject assertThatQueryException(String query) throws Exception {
try {
query(query);
} catch (BadRequestException e) {
return assertThat(e);
}
throw new AssertionError("expected BadRequestException");
}
@FunctionalInterface
private interface AddReviewerCaller {
void call(String changeId, String reviewer) throws RestApiException;
}
private static class TestWorkInProgressStateChangedListener
implements WorkInProgressStateChangedListener {
boolean invoked;
Boolean wip;
@Override
public void onWorkInProgressStateChanged(WorkInProgressStateChangedListener.Event event) {
this.invoked = true;
this.wip =
event.getChange().workInProgress != null ? event.getChange().workInProgress : false;
}
}
public static class TestAttentionSetListenerModule extends AbstractModule {
@Override
public void configure() {
DynamicSet.bind(binder(), AttentionSetListener.class).to(TestAttentionSetListener.class);
}
public static class TestAttentionSetListener implements AttentionSetListener {
AttentionSetListener.Event lastEvent;
int firedCount;
@Inject
public TestAttentionSetListener() {}
@Override
public void onAttentionSetChanged(AttentionSetListener.Event event) {
firedCount++;
lastEvent = event;
}
}
}
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}
private static class TestCommitValidationListener implements CommitValidationListener {
public CommitReceivedEvent receiveEvent;
@Override
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
this.receiveEvent = receiveEvent;
return ImmutableList.of();
}
}
}