// 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.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.TruthJUnit.assume; 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.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_ACCOUNTS; import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS; import static com.google.gerrit.extensions.client.ListChangesOption.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.reviewdb.client.RefNames.changeMetaRef; import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb; 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.Util.category; import static com.google.gerrit.server.project.Util.value; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.junit.Assert.fail; 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.util.concurrent.AtomicLongMap; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AcceptanceTestRequestScope; import com.google.gerrit.acceptance.GerritConfig; 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.common.FooterConstants; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.LabelFunction; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.AddReviewerResult; import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; import com.google.gerrit.extensions.api.changes.DeleteVoteInput; 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.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.RevisionApi; import com.google.gerrit.extensions.api.changes.StarsInput; 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.ProjectApi; 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.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.MergeInput; import com.google.gerrit.extensions.common.MergePatchSetInput; import com.google.gerrit.extensions.common.PureRevertInfo; import com.google.gerrit.extensions.common.RevisionInfo; import com.google.gerrit.extensions.common.TrackingIdInfo; import com.google.gerrit.extensions.events.ChangeIndexedListener; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.registration.RegistrationHandle; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; 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.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.LabelId; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.PostReview; import com.google.gerrit.server.config.AnonymousCowardNameProvider; import com.google.gerrit.server.git.ChangeMessageModifier; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.project.Util; 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.testutil.FakeEmailSender.Message; import com.google.gerrit.testutil.TestTimeUtil; import com.google.inject.Inject; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; 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.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.PushResult; import org.junit.After; import org.junit.Before; import org.junit.Test; @NoHttpd public class ChangeIT extends AbstractDaemonTest { private String systemTimeZone; @Inject private DynamicSet changeMessageModifiers; @Inject private DynamicSet changeIndexedListeners; private ChangeIndexedCounter changeIndexedCounter; private RegistrationHandle changeIndexedCounterHandle; @Before public void setTimeForTesting() { systemTimeZone = System.setProperty("user.timezone", "US/Eastern"); } @After public void resetTime() { TestTimeUtil.useSystemTime(); System.setProperty("user.timezone", systemTimeZone); } @Before public void addChangeIndexedCounter() { changeIndexedCounter = new ChangeIndexedCounter(); changeIndexedCounterHandle = changeIndexedListeners.add(changeIndexedCounter); } @After public void removeChangeIndexedCounter() { if (changeIndexedCounterHandle != null) { changeIndexedCounterHandle.remove(); } } @Test public void reflog() throws Exception { // Tests are using DfsRepository which does not implement getReflogReader, // so this will always fail. // TODO: change this if/when DfsRepository#getReflogReader is implemented. exception.expect(MethodNotAllowedException.class); exception.expectMessage("reflog not supported"); gApi.projects().name(project.get()).branch("master").reflog(); } @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).isTrue(); 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.getId().get()); assertThat(c.owner.name).isNull(); assertThat(c.owner.email).isNull(); assertThat(c.owner.username).isNull(); assertThat(c.owner.avatars).isNull(); } @Test public void setPrivateByOwner() throws Exception { TestRepository userRepo = cloneProject(project, user); PushOneCommit.Result result = pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); setApiUser(user); String changeId = result.getChangeId(); assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); gApi.changes().id(changeId).setPrivate(true, null); ChangeInfo info = gApi.changes().id(changeId).get(); assertThat(info.isPrivate).isTrue(); assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private"); assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE); gApi.changes().id(changeId).setPrivate(false, null); info = gApi.changes().id(changeId).get(); assertThat(info.isPrivate).isNull(); assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private"); assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE); String msg = "This is a security fix that must not be public."; gApi.changes().id(changeId).setPrivate(true, msg); info = gApi.changes().id(changeId).get(); assertThat(info.isPrivate).isTrue(); assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private\n\n" + msg); assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE); msg = "After this security fix has been released we can make it public now."; gApi.changes().id(changeId).setPrivate(false, msg); info = gApi.changes().id(changeId).get(); assertThat(info.isPrivate).isNull(); assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private\n\n" + msg); assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE); } @Test public void administratorCanSetUserChangePrivate() throws Exception { TestRepository userRepo = cloneProject(project, user); PushOneCommit.Result result = pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); String changeId = result.getChangeId(); assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); gApi.changes().id(changeId).setPrivate(true, null); setApiUser(user); ChangeInfo info = gApi.changes().id(changeId).get(); assertThat(info.isPrivate).isTrue(); } @Test public void cannotSetOtherUsersChangePrivate() throws Exception { PushOneCommit.Result result = createChange(); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("not allowed to mark private"); gApi.changes().id(result.getChangeId()).setPrivate(true, null); } @Test public void accessPrivate() throws Exception { TestRepository userRepo = cloneProject(project, user); PushOneCommit.Result result = pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); setApiUser(user); gApi.changes().id(result.getChangeId()).setPrivate(true, null); // Owner can always access its private changes. assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue(); // Add admin as a reviewer. gApi.changes().id(result.getChangeId()).addReviewer(admin.getId().toString()); // This change should be visible for admin as a reviewer. setApiUser(admin); assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue(); // Remove admin from reviewers. gApi.changes().id(result.getChangeId()).reviewer(admin.getId().toString()).remove(); // This change should not be visible for admin anymore. exception.expect(ResourceNotFoundException.class); exception.expectMessage("Not found: " + result.getChangeId()); gApi.changes().id(result.getChangeId()); } @Test public void privateChangeOfOtherUserCanBeAccessedWithPermission() throws Exception { PushOneCommit.Result result = createChange(); gApi.changes().id(result.getChangeId()).setPrivate(true, null); allow("refs/*", Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS); setApiUser(user); assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue(); } @Test public void administratorCanUnmarkPrivateAfterMerging() throws Exception { PushOneCommit.Result result = createChange(); String changeId = result.getChangeId(); gApi.changes().id(changeId).setPrivate(true, null); assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue(); merge(result); gApi.changes().id(changeId).setPrivate(false, null); assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); } @Test public void administratorCanMarkPrivateAfterMerging() throws Exception { PushOneCommit.Result result = createChange(); String changeId = result.getChangeId(); assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); merge(result); gApi.changes().id(changeId).setPrivate(true, null); assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue(); } @Test public void ownerCannotMarkPrivateAfterMerging() throws Exception { TestRepository userRepo = cloneProject(project, user); PushOneCommit.Result result = pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); String changeId = result.getChangeId(); assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); merge(result); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("not allowed to mark private"); gApi.changes().id(changeId).setPrivate(true, null); } @Test public void ownerCanUnmarkPrivateAfterMerging() throws Exception { TestRepository userRepo = cloneProject(project, user); PushOneCommit.Result result = pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); String changeId = result.getChangeId(); assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); gApi.changes().id(changeId).addReviewer(admin.getId().toString()); gApi.changes().id(changeId).setPrivate(true, null); assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue(); merge(result); setApiUser(user); gApi.changes().id(changeId).setPrivate(false, null); assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); } @Test public void setWorkInProgressNotAllowedWithoutPermission() throws Exception { PushOneCommit.Result rwip = createChange(); String changeId = rwip.getChangeId(); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("not allowed to set work in progress"); gApi.changes().id(changeId).setWorkInProgress(); } @Test public void setWorkInProgressAllowedAsAdmin() throws Exception { setApiUser(user); String changeId = gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id; setApiUser(admin); gApi.changes().id(changeId).setWorkInProgress(); assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue(); } @Test public void setWorkInProgressAllowedAsProjectOwner() throws Exception { setApiUser(user); String changeId = gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id; com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2(); grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS); setApiUser(user2); 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(); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("not allowed to set ready for review"); gApi.changes().id(changeId).setReadyForReview(); } @Test public void setReadyForReviewAllowedAsAdmin() throws Exception { setApiUser(user); String changeId = gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id; gApi.changes().id(changeId).setWorkInProgress(); setApiUser(admin); gApi.changes().id(changeId).setReadyForReview(); assertThat(gApi.changes().id(changeId).get().workInProgress).isNull(); } @Test public void setReadyForReviewAllowedAsProjectOwner() throws Exception { setApiUser(user); 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(); grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS); setApiUser(user2); 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 pendingReviewersInNoteDb() throws Exception { assume().that(notesMigration.readChanges()).isTrue(); 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. TestAccount user1 = accountCreator.create(name("user1"), name("user1") + "@example.com", "User 1"); TestAccount user2 = accountCreator.create(name("user2"), name("user2") + "@example.com", "User 2"); TestAccount user3 = accountCreator.create(name("user3"), name("user3") + "@example.com", "User 3"); TestAccount user4 = accountCreator.create(name("user4"), name("user4") + "@example.com", "User 4"); ReviewInput in = ReviewInput.noScore() .reviewer(user1.email) .reviewer(user2.email) .reviewer(user3.email, CC, false) .reviewer(user4.email, 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).revision("current").review(in); assertThat(result.reviewers).isNotEmpty(); ChangeInfo info = gApi.changes().id(changeId).get(); Function, Collection> toEmails = ais -> ais.stream().map(ai -> ai.email).collect(toSet()); assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER))) .containsExactly( admin.email, user1.email, user2.email, "byemail1@example.com", "byemail2@example.com"); assertThat(toEmails.apply(info.pendingReviewers.get(CC))) .containsExactly(user3.email, user4.email, "byemail3@example.com", "byemail4@example.com"); assertThat(info.pendingReviewers.get(REMOVED)).isNull(); // Stage some pending reviewer removals. gApi.changes().id(changeId).reviewer(user1.email).remove(); gApi.changes().id(changeId).reviewer(user3.email).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(admin.email, user2.email, "byemail2@example.com"); assertThat(toEmails.apply(info.pendingReviewers.get(CC))) .containsExactly(user4.email, "byemail4@example.com"); assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED))) .containsExactly(user1.email, user3.email, "byemail1@example.com", "byemail3@example.com"); // "Undo" a removal. in = ReviewInput.noScore().reviewer(user1.email); gApi.changes().id(changeId).revision("current").review(in); info = gApi.changes().id(changeId).get(); assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER))) .containsExactly(admin.email, user1.email, user2.email, "byemail2@example.com"); assertThat(toEmails.apply(info.pendingReviewers.get(CC))) .containsExactly(user4.email, "byemail4@example.com"); assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED))) .containsExactly(user3.email, "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(admin.email, user1.email, user2.email, "byemail2@example.com"); assertThat(toEmails.apply(info.reviewers.get(CC))) .containsExactly(user4.email, "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 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()).revision("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(); r.assertOkStatus(); assertThat(r.getChange().change().isWorkInProgress()).isFalse(); ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); ReviewResult result = gApi.changes().id(r.getChangeId()).revision("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(); r.assertOkStatus(); assertThat(r.getChange().change().isWorkInProgress()).isFalse(); ReviewInput in = ReviewInput.approve().reviewer(user.email).label("Code-Review", 1).setWorkInProgress(true); gApi.changes().id(r.getChangeId()).revision("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("Code-Review").recommended._accountId).isEqualTo(admin.id.get()); } @Test public void reviewWithWorkInProgressAndReadyReturnsError() throws Exception { PushOneCommit.Result r = createChange(); r.assertOkStatus(); ReviewInput in = ReviewInput.noScore(); in.ready = true; in.workInProgress = true; ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in); assertThat(result.error).isEqualTo(PostReview.ERROR_WIP_READY_MUTUALLY_EXCLUSIVE); } @Test public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception { PushOneCommit.Result r = createChange(); r.assertOkStatus(); ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); setApiUser(user); ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in); assertThat(result.error).isEqualTo(PostReview.ERROR_ONLY_OWNER_CAN_MODIFY_WORK_IN_PROGRESS); } @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( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT, changeId); PushOneCommit.Result r2 = push2.to("refs/for/other"); assertThat(r2.getChangeId()).isEqualTo(changeId); exception.expect(ResourceNotFoundException.class); exception.expectMessage("Multiple changes found for " + changeId); gApi.changes().id(changeId).get(); } @Test public void revert() 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 revertChange = gApi.changes().id(r.getChangeId()).revert().get(); // expected messages on source change: // 1. Uploaded patch set 1. // 2. Patch Set 1: Code-Review+2 // 3. Change has been successfully merged by Administrator // 4. Patch Set 1: Reverted List sourceMessages = new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages); assertThat(sourceMessages).hasSize(4); String expectedMessage = String.format("Created a revert of this change as %s", revertChange.changeId); assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage); assertThat(revertChange.messages).hasSize(1); assertThat(revertChange.messages.iterator().next().message).isEqualTo("Uploaded patch set 1."); assertThat(revertChange.revertOf).isEqualTo(gApi.changes().id(r.getChangeId()).get()._number); } @Test public void revertNotifications() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).addReviewer(user.email); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); sender.clear(); ChangeInfo revertChange = gApi.changes().id(r.getChangeId()).revert().get(); List messages = sender.getMessages(); assertThat(messages).hasSize(2); assertThat(sender.getMessages(revertChange.changeId, "newchange")).hasSize(1); assertThat(sender.getMessages(r.getChangeId(), "revert")).hasSize(1); } @Test public void revertPreservesReviewersAndCcs() throws Exception { PushOneCommit.Result r = createChange(); ReviewInput in = ReviewInput.approve(); in.reviewer(user.email); in.reviewer(accountCreator.user2().email, ReviewerState.CC, true); // Add user as reviewer that will create the revert in.reviewer(accountCreator.admin2().email); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); // expect both the original reviewers and CCs to be preserved // original owner should be added as reviewer, user requesting the revert (new owner) removed setApiUser(accountCreator.admin2()); Map> result = gApi.changes().id(r.getChangeId()).revert().get().reviewers; assertThat(result).containsKey(ReviewerState.REVIEWER); List reviewers = result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList()); if (notesMigration.readChanges()) { assertThat(result).containsKey(ReviewerState.CC); List ccs = result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList()); assertThat(ccs).containsExactly(accountCreator.user2().id.get()); assertThat(reviewers).containsExactly(user.id.get(), admin.id.get()); } else { assertThat(reviewers) .containsExactly(user.id.get(), admin.id.get(), accountCreator.user2().id.get()); } } @Test @TestProjectInput(createEmptyCommit = false) public void revertInitialCommit() 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(); exception.expect(ResourceConflictException.class); exception.expectMessage("Cannot revert initial commit"); gApi.changes().id(r.getChangeId()).revert(); } @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("Code-Review"); assertThat(cr).isNotNull(); assertThat(cr.all).hasSize(1); assertThat(cr.all.get(0).value).isEqualTo(1); if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) { // Ensure record was actually copied under ReviewDb List psas = unwrapDb(db) .patchSetApprovals() .byPatchSet(new PatchSet.Id(new Change.Id(c2._number), 2)) .toList(); assertThat(psas).hasSize(1); assertThat(psas.get(0).getValue()).isEqualTo((short) 1); } // Rebasing the second change again should fail exception.expect(ResourceConflictException.class); exception.expectMessage("Change is already up to date"); gApi.changes().id(changeId).current().rebase(); } @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(); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("rebase not permitted"); gApi.changes().id(changeId).rebase(); } @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(); grant(project, "refs/heads/master", Permission.REBASE, false, REGISTERED_USERS); // Rebase the second String changeId = r2.getChangeId(); setApiUser(user); 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(); grant(project, "refs/heads/master", Permission.REBASE, false, REGISTERED_USERS); block("refs/for/*", Permission.PUSH, REGISTERED_USERS); // Rebase the second String changeId = r2.getChangeId(); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("rebase not permitted"); gApi.changes().id(changeId).rebase(); } @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(); block("refs/for/*", Permission.PUSH, REGISTERED_USERS); // Rebase the second String changeId = r2.getChangeId(); exception.expect(AuthException.class); exception.expectMessage("rebase not permitted"); gApi.changes().id(changeId).rebase(); } @Test public void deleteNewChangeAsAdmin() throws Exception { deleteChangeAsUser(admin, admin); } @Test @TestProjectInput(cloneAs = "user") public void deleteNewChangeAsNormalUser() throws Exception { PushOneCommit.Result changeResult = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); String changeId = changeResult.getChangeId(); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } @Test public void deleteNewChangeAsUserWithDeleteChangesPermissionForGroup() throws Exception { allow("refs/*", Permission.DELETE_CHANGES, REGISTERED_USERS); deleteChangeAsUser(admin, user); } @Test public void deleteNewChangeAsUserWithDeleteChangesPermissionForProjectOwners() throws Exception { GroupApi groupApi = gApi.groups().create(name("delete-change")); groupApi.addMembers("user"); ProjectInput in = new ProjectInput(); in.name = name("delete-change"); in.owners = Lists.newArrayListWithCapacity(1); in.owners.add(groupApi.name()); in.createEmptyCommit = true; ProjectApi api = gApi.projects().create(in); Project.NameKey nameKey = new Project.NameKey(api.get().name); ProjectConfig cfg = projectCache.checkedGet(nameKey).getConfig(); Util.allow(cfg, Permission.DELETE_CHANGES, PROJECT_OWNERS, "refs/*"); saveProjectConfig(nameKey, cfg); deleteChangeAsUser(nameKey, admin, user); } @Test public void deleteChangeAsUserWithDeleteOwnChangesPermissionForGroup() throws Exception { allow("refs/*", Permission.DELETE_OWN_CHANGES, REGISTERED_USERS); deleteChangeAsUser(user, user); } @Test public void deleteChangeAsUserWithDeleteOwnChangesPermissionForOwners() throws Exception { allow("refs/*", Permission.DELETE_OWN_CHANGES, CHANGE_OWNER); deleteChangeAsUser(user, user); } private void deleteChangeAsUser(TestAccount owner, TestAccount deleteAs) throws Exception { deleteChangeAsUser(project, owner, deleteAs); } private void deleteChangeAsUser( Project.NameKey projectName, TestAccount owner, TestAccount deleteAs) throws Exception { try { setApiUser(owner); 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()); setApiUser(deleteAs); gApi.changes().id(changeId).delete(); assertThat(query(changeId)).isEmpty(); String ref = new Change.Id(id).toRefPrefix() + "1"; eventRecorder.assertRefUpdatedEvents(projectName.get(), ref, null, commit, commit, null); } finally { removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES); removePermission(project, "refs/*", Permission.DELETE_CHANGES); } } @Test public void deleteNewChangeOfAnotherUserAsAdmin() throws Exception { deleteChangeAsUser(user, admin); } @Test public void deleteNewChangeOfAnotherUserWithDeleteOwnChangesPermission() throws Exception { allow("refs/*", Permission.DELETE_OWN_CHANGES, REGISTERED_USERS); try { PushOneCommit.Result changeResult = createChange(); String changeId = changeResult.getChangeId(); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } finally { removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES); } } @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 = "user") public void deleteAbandonedChangeAsNormalUser() throws Exception { PushOneCommit.Result changeResult = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); String changeId = changeResult.getChangeId(); setApiUser(user); gApi.changes().id(changeId).abandon(); exception.expect(AuthException.class); exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } @Test @TestProjectInput(cloneAs = "user") public void deleteAbandonedChangeOfAnotherUserAsAdmin() throws Exception { PushOneCommit.Result changeResult = pushFactory.create(db, user.getIdent(), 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); exception.expect(MethodNotAllowedException.class); exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } @Test @TestProjectInput(cloneAs = "user") public void deleteMergedChangeWithDeleteOwnChangesPermission() throws Exception { allow("refs/*", Permission.DELETE_OWN_CHANGES, REGISTERED_USERS); try { PushOneCommit.Result changeResult = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); String changeId = changeResult.getChangeId(); merge(changeResult); setApiUser(user); exception.expect(MethodNotAllowedException.class); exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } finally { removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES); } } @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); exception.expect(ResourceConflictException.class); exception.expectMessage( String.format("Cannot delete change %s: patch set 1 is already merged", id)); gApi.changes().id(changeId).delete(); } @Test public void rebaseUpToDateChange() throws Exception { PushOneCommit.Result r = createChange(); exception.expect(ResourceConflictException.class); exception.expectMessage("Change is already up to date"); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).rebase(); } @Test public void rebaseConflict() 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(); PushOneCommit push = pushFactory.create( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, "other content", "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5"); r = push.to("refs/for/master"); r.assertOkStatus(); exception.expect(ResourceConflictException.class); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).rebase(); } @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.getId().get()).isEqualTo(2); // rebase r2 onto r3 (referenced by ref) ri.base = ps3.getId().toRefName(); gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri); PatchSet ps2 = r2.getPatchSet(); assertThat(ps2.getId().get()).isEqualTo(2); // rebase r1 onto r2 (referenced by commit) ri.base = ps2.getRevision().get(); gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri); PatchSet ps1 = r1.getPatchSet(); assertThat(ps1.getId().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.getRevision().get()).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"; exception.expect(ResourceConflictException.class); exception.expectMessage(expectedMessage); gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri); } @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 = get(changeId); assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); exception.expect(ResourceConflictException.class); exception.expectMessage("change is abandoned"); gApi.changes().id(changeId).revision(r.getCommit().name()).rebase(); } @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 = get(changeId); assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); RebaseInput ri = new RebaseInput(); ri.base = r.getCommit().name(); exception.expect(ResourceConflictException.class); exception.expectMessage("base change is abandoned: " + changeId); gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri); } @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; exception.expect(ResourceConflictException.class); exception.expectMessage("cannot rebase change onto itself"); gApi.changes().id(changeId).revision(commit).rebase(ri); } @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(db, user.getIdent(), 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 reviewer Collection reviewers = change.reviewers.get(REVIEWER); assertThat(reviewers).isNotNull(); assertThat(reviewers).hasSize(1); assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get()); assertThat(change.reviewers.get(CC)).isNull(); List messages = sender.getMessages(); assertThat(messages).hasSize(1); Message m = messages.get(0); assertThat(m.rcpt()).containsExactly(user.emailAddress); assertThat(m.body()).contains(admin.fullName + " 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 = createProject("p"); ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); Util.allow( cfg, Permission.READ, groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID(), "refs/*"); Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*"); saveProjectConfig(p, cfg); // admin pushes commit of user TestRepository repo = cloneProject(p, admin); PushOneCommit push = pushFactory.create(db, user.getIdent(), 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 setApiUser(user); try { gApi.changes().id(result.getChangeId()).get(); fail("Expected ResourceNotFoundException"); } catch (ResourceNotFoundException e) { // Expected. } // 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( db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT + "\n\n" + FooterConstants.REVIEWED_BY.getName() + ": " + user.getIdent().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 reviewers = change.reviewers.get(REVIEWER); assertThat(reviewers).isNotNull(); assertThat(reviewers).hasSize(1); assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get()); assertThat(change.reviewers.get(CC)).isNull(); List messages = sender.getMessages(); assertThat(messages).hasSize(1); Message m = messages.get(0); assertThat(m.rcpt()).containsExactly(user.emailAddress); 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 = createProject("p"); ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); Util.allow( cfg, Permission.READ, groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID(), "refs/*"); Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*"); saveProjectConfig(p, cfg); // admin pushes commit that references 'user' in a footer TestRepository repo = cloneProject(p, admin); PushOneCommit push = pushFactory.create( db, admin.getIdent(), repo, PushOneCommit.SUBJECT + "\n\n" + FooterConstants.REVIEWED_BY.getName() + ": " + user.getIdent().toExternalString(), PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT); PushOneCommit.Result result = push.to("refs/for/master"); result.assertOkStatus(); // check that 'user' cannot see the change setApiUser(user); try { gApi.changes().id(result.getChangeId()).get(); fail("Expected ResourceNotFoundException"); } catch (ResourceNotFoundException e) { // Expected. } // check that 'user' was NOT added as cc ('user' can't see the change) setApiUser(admin); 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 = createProject("p"); ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); Util.allow( cfg, Permission.READ, groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID(), "refs/*"); Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*"); saveProjectConfig(p, cfg); // create change TestRepository repo = cloneProject(p, admin); PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo); PushOneCommit.Result result = push.to("refs/for/master"); result.assertOkStatus(); // check the user cannot see the change setApiUser(user); try { gApi.changes().id(result.getChangeId()).get(); fail("Expected ResourceNotFoundException"); } catch (ResourceNotFoundException e) { // Expected. } // try to add user as reviewer setApiUser(admin); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; AddReviewerResult 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 addReviewerThatIsInactive() throws Exception { PushOneCommit.Result result = createChange(); String username = name("new-user"); gApi.accounts().create(username).setActive(false); AddReviewerInput in = new AddReviewerInput(); in.reviewer = username; AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in); assertThat(r.input).isEqualTo(username); assertThat(r.error).contains("identifies an inactive account"); assertThat(r.reviewers).isNull(); } @Test public void addReviewerThatIsInactiveEmailFallback() throws Exception { assume().that(notesMigration.readChanges()).isTrue(); ConfigInput conf = new ConfigInput(); conf.enableReviewerByEmail = InheritableBoolean.TRUE; gApi.projects().name(project.get()).config(conf); PushOneCommit.Result result = createChange(); String username = "user@domain.com"; gApi.accounts().create(username).setActive(false); AddReviewerInput in = new AddReviewerInput(); in.reviewer = username; in.state = ReviewerState.CC; AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in); assertThat(r.input).isEqualTo(username); assertThat(r.error).isNull(); // When adding by email, the reviewers field is also empty because we can't // render a ReviewerInfo object for a non-account. assertThat(r.reviewers).isNull(); } @Test public void addReviewer() throws Exception { TestTimeUtil.resetWithClockStep(1, SECONDS); PushOneCommit.Result r = createChange(); ChangeResource rsrc = parseResource(r); String oldETag = rsrc.getETag(); Timestamp oldTs = rsrc.getChange().getLastUpdatedOn(); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; gApi.changes().id(r.getChangeId()).addReviewer(in); List messages = sender.getMessages(); assertThat(messages).hasSize(1); Message m = messages.get(0); assertThat(m.rcpt()).containsExactly(user.emailAddress); 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(); // When NoteDb is enabled adding a reviewer records that user as reviewer // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0 // approval on the change which is treated as CC when the ChangeInfo is // created. Collection reviewers = c.reviewers.get(REVIEWER); assertThat(reviewers).isNotNull(); assertThat(reviewers).hasSize(1); assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get()); // 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(); gApi.accounts().id(user.id.get()).setStatus("new status"); rsrc = parseResource(r); assertThat(rsrc.getETag()).isNotEqualTo(oldETag); } @Test public void notificationsForAddedWorkInProgressReviewers() throws Exception { AddReviewerInput in = new AddReviewerInput(); 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()).hasSize(0); // 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()).revision("current").review(batchIn); assertThat(sender.getMessages()).hasSize(0); // Top-level notify property can force notifications when adding reviewer // via PostReview. r = createWorkInProgressChange(); batchIn.notify = NotifyHandling.OWNER_REVIEWERS; gApi.changes().id(r.getChangeId()).revision("current").review(batchIn); assertThat(sender.getMessages()).hasSize(1); } @Test public void addReviewerThatIsNotPerfectMatch() throws Exception { TestTimeUtil.resetWithClockStep(1, SECONDS); PushOneCommit.Result r = createChange(); ChangeResource rsrc = parseResource(r); String oldETag = rsrc.getETag(); Timestamp oldTs = rsrc.getChange().getLastUpdatedOn(); // create a group named "ab" with one user: testUser TestAccount testUser = accountCreator.create("abcd", "abcd@test.com", "abcd"); String testGroup = createGroupWithRealName("ab"); GroupApi groupApi = gApi.groups().id(testGroup); groupApi.description("test group"); groupApi.addMembers(user.fullName); AddReviewerInput in = new AddReviewerInput(); in.reviewer = "abc"; gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer); List messages = sender.getMessages(); assertThat(messages).hasSize(1); Message m = messages.get(0); assertThat(m.rcpt()).containsExactly(testUser.emailAddress); assertThat(m.body()).contains("Hello " + testUser.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, testUser.email); ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); // When NoteDb is enabled adding a reviewer records that user as reviewer // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0 // approval on the change which is treated as CC when the ChangeInfo is // created. Collection reviewers = c.reviewers.get(REVIEWER); assertThat(reviewers).isNotNull(); assertThat(reviewers).hasSize(1); assertThat(reviewers.iterator().next()._accountId).isEqualTo(testUser.getId().get()); // Ensure ETag and lastUpdatedOn are updated. rsrc = parseResource(r); assertThat(rsrc.getETag()).isNotEqualTo(oldETag); assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs); } @Test public void addGroupAsReviewersWhenANotPerfectMatchedUserExists() throws Exception { TestTimeUtil.resetWithClockStep(1, SECONDS); PushOneCommit.Result r = createChange(); ChangeResource rsrc = parseResource(r); String oldETag = rsrc.getETag(); Timestamp oldTs = rsrc.getChange().getLastUpdatedOn(); // create a group named "kobe" with one user: lee TestAccount testUser = accountCreator.create("kobebryant", "kobebryant@test.com", "kobebryant"); TestAccount myGroupUser = accountCreator.create("lee", "lee@test.com", "lee"); String testGroup = createGroupWithRealName("kobe"); GroupApi groupApi = gApi.groups().id(testGroup); groupApi.description("test group"); groupApi.addMembers(myGroupUser.fullName); // ensure that user "user" is not in the group groupApi.removeMembers(testUser.fullName); AddReviewerInput in = new AddReviewerInput(); in.reviewer = testGroup; gApi.changes().id(r.getChangeId()).addReviewer(in.reviewer); List messages = sender.getMessages(); assertThat(messages).hasSize(1); Message m = messages.get(0); assertThat(m.rcpt()).containsExactly(myGroupUser.emailAddress); assertThat(m.body()).contains("Hello " + myGroupUser.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, myGroupUser.email); ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); // When NoteDb is enabled adding a reviewer records that user as reviewer // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0 // approval on the change which is treated as CC when the ChangeInfo is // created. Collection reviewers = c.reviewers.get(REVIEWER); assertThat(reviewers).isNotNull(); assertThat(reviewers).hasSize(1); assertThat(reviewers.iterator().next()._accountId).isEqualTo(myGroupUser.getId().get()); // Ensure ETag and lastUpdatedOn are updated. rsrc = parseResource(r); assertThat(rsrc.getETag()).isNotEqualTo(oldETag); assertThat(rsrc.getChange().getLastUpdatedOn()).isNotEqualTo(oldTs); } @Test public void addReviewerWithNoteDbWhenDummyApprovalInReviewDbExists() throws Exception { assume().that(notesMigration.readChanges()).isTrue(); assume().that(notesMigration.changePrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB); PushOneCommit.Result r = createChange(); // insert dummy approval in ReviewDb PatchSetApproval psa = new PatchSetApproval( new PatchSetApproval.Key(r.getPatchSetId(), user.id, new LabelId("Code-Review")), (short) 0, TimeUtil.nowTs()); db.patchSetApprovals().insert(Collections.singleton(psa)); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; gApi.changes().id(r.getChangeId()).addReviewer(in); } @Test public void addSelfAsReviewer() throws Exception { TestTimeUtil.resetWithClockStep(1, SECONDS); PushOneCommit.Result r = createChange(); ChangeResource rsrc = parseResource(r); String oldETag = rsrc.getETag(); Timestamp oldTs = rsrc.getChange().getLastUpdatedOn(); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; setApiUser(user); gApi.changes().id(r.getChangeId()).addReviewer(in); // There should be no email notification when adding self assertThat(sender.getMessages()).isEmpty(); // When NoteDb is enabled adding a reviewer records that user as reviewer // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0 // approval on the change which is treated as CC when the ChangeInfo is // created. ChangeInfo c = gApi.changes().id(r.getChangeId()).get(); Collection reviewers = c.reviewers.get(REVIEWER); assertThat(reviewers).isNotNull(); assertThat(reviewers).hasSize(1); assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().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 { PushOneCommit.Result r = createChange(); setApiUser(user); assertThat(getReviewerState(r.getChangeId(), user.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); // If we're not reading from NoteDb, then the CCed user will be returned in the REVIEWER state. assertThat(getReviewerState(r.getChangeId(), user.id)) .hasValue(notesMigration.readChanges() ? CC : REVIEWER); } @Test public void implicitlyCcOnNonVotingReviewGwtStyle() throws Exception { PushOneCommit.Result r = createChange(); setApiUser(user); assertThat(getReviewerState(r.getChangeId(), user.id)).isEmpty(); // Exact request format made by GWT UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8. ReviewInput in = new ReviewInput(); in.labels = ImmutableMap.of("Code-Review", (short) 0); in.drafts = DraftHandling.PUBLISH_ALL_REVISIONS; in.message = "comment"; gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in); // If we're not reading from NoteDb, then the CCed user will be returned in the REVIEWER state. assertThat(getReviewerState(r.getChangeId(), user.id)) .hasValue(notesMigration.readChanges() ? CC : REVIEWER); } @Test public void implicitlyAddReviewerOnVotingReview() throws Exception { PushOneCommit.Result r = createChange(); setApiUser(user); 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 if we're using NoteDb. setApiUser(admin); gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).remove(); c = gApi.changes().id(r.getChangeId()).get(); assertThat(c.reviewers.values()).isEmpty(); setApiUser(user); gApi.changes() .id(r.getChangeId()) .revision(r.getCommit().name()) .review(new ReviewInput().message("hi")); c = gApi.changes().id(r.getChangeId()).get(); ReviewerState state = notesMigration.readChanges() ? CC : REVIEWER; assertThat(c.reviewers.get(state).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 reviewers = c.reviewers.get(REVIEWER); assertThat(reviewers).hasSize(1); assertThat(reviewers.iterator().next()._accountId).isEqualTo(admin.getId().get()); assertThat(c.reviewers).doesNotContainKey(CC); AddReviewerInput in = new AddReviewerInput(); 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 reviewerIt = reviewers.iterator(); assertThat(reviewerIt.next()._accountId).isEqualTo(admin.getId().get()); assertThat(reviewerIt.next()._accountId).isEqualTo(user.getId().get()); assertThat(c.reviewers).doesNotContainKey(CC); } @Test public void eTagChangesWhenOwnerUpdatesAccountStatus() throws Exception { PushOneCommit.Result r = createChange(); ChangeResource rsrc = parseResource(r); String oldETag = rsrc.getETag(); gApi.accounts().id(admin.id.get()).setStatus("new status"); rsrc = parseResource(r); assertThat(rsrc.getETag()).isNotEqualTo(oldETag); } @Test public void emailNotificationForFileLevelComment() throws Exception { String changeId = createChange().getChangeId(); AddReviewerInput in = new AddReviewerInput(); 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.emailAddress); } @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)); exception.expect(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 m = gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).votes(); assertThat(m).hasSize(1); assertThat(m).containsEntry("Code-Review", Short.valueOf((short) 2)); setApiUser(user); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.dislike()); m = gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).votes(); assertThat(m).hasSize(1); assertThat(m).containsEntry("Code-Review", Short.valueOf((short) -1)); } @Test public void removeReviewerNoVotes() throws Exception { ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); LabelType verified = category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); cfg.getLabelSections().put(verified.getName(), verified); AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); String heads = RefNames.REFS_HEADS + "*"; Util.allow(cfg, Permission.forLabel(Util.verified().getName()), -1, 1, registeredUsers, heads); saveProjectConfig(project, cfg); PushOneCommit.Result r = createChange(); String changeId = r.getChangeId(); gApi.changes().id(changeId).addReviewer(user.getId().toString()); // ReviewerState will vary between ReviewDb and NoteDb; we just care that it // shows up somewhere. Iterable reviewers = Iterables.concat(gApi.changes().id(changeId).get().reviewers.values()); assertThat(reviewers).hasSize(1); assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get()); sender.clear(); gApi.changes().id(changeId).reviewer(user.getId().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.fullName + "."); assertThat(message.body()).doesNotContain("with the following votes"); // Make sure the reviewer can still be added again. gApi.changes().id(changeId).addReviewer(user.getId().toString()); reviewers = Iterables.concat(gApi.changes().id(changeId).get().reviewers.values()); assertThat(reviewers).hasSize(1); assertThat(reviewers.iterator().next()._accountId).isEqualTo(user.getId().get()); // Remove again, and then try to remove once more to verify 404 is // returned. gApi.changes().id(changeId).reviewer(user.getId().toString()).remove(); exception.expect(ResourceNotFoundException.class); gApi.changes().id(changeId).reviewer(user.getId().toString()).remove(); } @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()); setApiUser(user); gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.recommend()); Collection reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER); assertThat(reviewers).hasSize(2); Iterator reviewerIt = reviewers.iterator(); assertThat(reviewerIt.next()._accountId).isEqualTo(admin.getId().get()); assertThat(reviewerIt.next()._accountId).isEqualTo(user.getId().get()); sender.clear(); setApiUser(admin); DeleteReviewerInput input = new DeleteReviewerInput(); if (!notify) { input.notify = NotifyHandling.NONE; } gApi.changes().id(changeId).reviewer(user.getId().toString()).remove(input); if (notify) { assertThat(sender.getMessages()).hasSize(1); Message message = sender.getMessages().get(0); assertThat(message.body()) .contains("Removed reviewer " + user.fullName + " with the following votes"); assertThat(message.body()).contains("* Code-Review+1 by " + user.fullName); } 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.getId().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()); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("remove reviewer not permitted"); gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).remove(); } @Test public void deleteVote() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); setApiUser(user); recommend(r.getChangeId()); setApiUser(admin); sender.clear(); gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote("Code-Review"); List messages = sender.getMessages(); assertThat(messages).hasSize(1); Message msg = messages.get(0); assertThat(msg.rcpt()).containsExactly(user.emailAddress); assertThat(msg.body()).contains(admin.fullName + " has removed a vote on this change.\n"); assertThat(msg.body()) .contains("Removed Code-Review+1 by " + user.fullName + " <" + user.email + ">\n"); Map m = gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).votes(); // Dummy 0 approval on the change to block vote copying to this patch set. assertThat(m).containsExactly("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.getId().get()); assertThat(message.message).isEqualTo("Removed Code-Review+1 by User \n"); assertThat(getReviewers(c.reviewers.get(REVIEWER))) .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId())); } @Test public void deleteVoteNotifyNone() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); setApiUser(user); recommend(r.getChangeId()); setApiUser(admin); sender.clear(); DeleteVoteInput in = new DeleteVoteInput(); in.label = "Code-Review"; in.notify = NotifyHandling.NONE; gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in); assertThat(sender.getMessages()).isEmpty(); } @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 = "Code-Review"; in.notify = NotifyHandling.NONE; // notify unrelated account as TO TestAccount user2 = accountCreator.user2(); setApiUser(user); recommend(r.getChangeId()); setApiUser(admin); sender.clear(); in.notifyDetails = new HashMap<>(); in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(user2.email))); gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in); assertNotifyTo(user2); // notify unrelated account as CC setApiUser(user); recommend(r.getChangeId()); setApiUser(admin); sender.clear(); in.notifyDetails = new HashMap<>(); in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(user2.email))); gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in); assertNotifyCc(user2); // notify unrelated account as BCC setApiUser(user); recommend(r.getChangeId()); setApiUser(admin); sender.clear(); in.notifyDetails = new HashMap<>(); in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(user2.email))); gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in); assertNotifyBcc(user2); } @Test public void deleteVoteNotPermitted() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("delete vote not permitted"); gApi.changes().id(r.getChangeId()).reviewer(admin.getId().toString()).deleteVote("Code-Review"); } @Test public void nonVotingReviewerStaysAfterSubmit() throws Exception { LabelType verified = category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); cfg.getLabelSections().put(verified.getName(), verified); String heads = "refs/heads/*"; AccountGroup.UUID owners = systemGroupBackend.getGroup(CHANGE_OWNER).getUUID(); AccountGroup.UUID registered = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); Util.allow(cfg, Permission.forLabel(verified.getName()), -1, 1, owners, heads); Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, registered, heads); saveProjectConfig(project, cfg); // 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.getId())); assertThat(c.reviewers.get(CC)).isNull(); // Add the user as reviewer AddReviewerInput in = new AddReviewerInput(); 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.getId(), user.getId())); // Approve the change as user, then remove the approval // (only to confirm that the user does have Code-Review+2 permission) setApiUser(user); gApi.changes().id(changeId).revision(commit).review(ReviewInput.approve()); gApi.changes().id(changeId).revision(commit).review(ReviewInput.noScore()); // Submit the change setApiUser(admin); 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.getId(), user.getId())); } @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(info.branch).isEqualTo(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 results = gApi.changes().query().get(); assertThat(results.size()).isAtLeast(1); List 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 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 results = gApi.changes().query().withLimit(1).get(); assertThat(results).hasSize(1); assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r2.getChangeId()); } @Test public void queryChangesStart() throws Exception { PushOneCommit.Result r1 = createChange(); createChange(); List 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("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.getId().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()); setApiUser(user); assertThat(query("owner:self project:{" + project.get() + "}")).isEmpty(); } @Test public void checkReviewedFlagBeforeAndAfterReview() throws Exception { PushOneCommit.Result r = createChange(); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; gApi.changes().id(r.getChangeId()).addReviewer(in); setApiUser(user); assertThat(get(r.getChangeId()).reviewed).isNull(); revision(r).review(ReviewInput.recommend()); assertThat(get(r.getChangeId()).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(""); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("edit topic name not permitted"); gApi.changes().id(r.getChangeId()).topic("mytopic"); } @Test public void editTopicWithPermissionAllowed() throws Exception { PushOneCommit.Result r = createChange(); assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo(""); grant(project, "refs/heads/master", Permission.EDIT_TOPIC_NAME, false, REGISTERED_USERS); setApiUser(user); 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(); disableChangeIndexWrites(); try { r = amendChange(r.getChangeId()); } finally { enableChangeIndexWrites(); } 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(ChangeStatus.MERGED); } @Test public void submitNotAllowedWithoutPermission() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); setApiUser(user); exception.expect(AuthException.class); exception.expectMessage("submit not permitted"); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); } @Test public void submitAllowedWithPermission() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); grant(project, "refs/heads/master", Permission.SUBMIT, false, REGISTERED_USERS); setApiUser(user); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED); } @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 = category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed")); LabelType custom1 = category("Custom1", value(1, "Positive"), value(0, "No score"), value(-1, "Negative")); LabelType custom2 = category("Custom2", value(1, "Positive"), value(0, "No score"), value(-1, "Negative")); ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); cfg.getLabelSections().put(verified.getName(), verified); cfg.getLabelSections().put(custom1.getName(), custom1); cfg.getLabelSections().put(custom2.getName(), custom2); String heads = "refs/heads/*"; AccountGroup.UUID anon = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID(); Util.allow(cfg, Permission.forLabel("Verified"), -1, 1, anon, heads); Util.allow(cfg, Permission.forLabel("Custom1"), -1, 1, anon, heads); Util.allow(cfg, Permission.forLabel("Custom2"), -1, 1, anon, heads); saveProjectConfig(project, cfg); PushOneCommit.Result r1 = createChange(); r1.assertOkStatus(); PushOneCommit.Result r2 = pushFactory .create( db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content", r1.getChangeId()) .to("refs/for/master"); r2.assertOkStatus(); ReviewInput in = new ReviewInput(); in.label("Code-Review", 1); in.label("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 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 expectedFooters = Arrays.asList( "Change-Id: " + r2.getChangeId(), "Reviewed-on: " + canonicalWebUrl.get() + r2.getChange().getId(), "Reviewed-by: Administrator ", "Custom2: Administrator ", "Tested-by: Administrator "); assertThat(footers).containsExactlyElementsIn(expectedFooters); } @Test public void customCommitFooters() throws Exception { PushOneCommit.Result change = createChange(); RegistrationHandle handle = changeMessageModifiers.add( new ChangeMessageModifier() { @Override public String onSubmit( String newCommitMessage, RevCommit original, RevCommit mergeTip, Branch.NameKey destination) { assertThat(original.getName()).isNotEqualTo(mergeTip.getName()); return newCommitMessage + "Custom: " + destination.get(); } }); ChangeInfo actual; try { actual = gApi.changes().id(change.getChangeId()).get(ALL_REVISIONS, COMMIT_FOOTERS); } finally { handle.remove(); } List 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 expectedFooters = Arrays.asList( "Change-Id: " + change.getChangeId(), "Reviewed-on: " + canonicalWebUrl.get() + change.getChange().getId(), "Custom: refs/heads/master"); assertThat(footers).containsExactlyElementsIn(expectedFooters); } @Test public void defaultSearchDoesNotTouchDatabase() throws Exception { setApiUser(admin); 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(); setApiUser(user); AcceptanceTestRequestScope.Context ctx = disableDb(); try { assertThat( gApi.changes() .query() .withQuery("project:{" + project.get() + "} (status:open OR status:closed)") // Options should match defaults in AccountDashboardScreen. .withOption(LABELS) .withOption(DETAILED_ACCOUNTS) .withOption(REVIEWED) .get()) .hasSize(2); } finally { enableDb(ctx); } } @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("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); ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); Util.blockLabel(cfg, "Code-Review", REGISTERED_USERS, "refs/heads/*"); saveProjectConfig(project, cfg); c = gApi.changes().id(triplet).get(DETAILED_LABELS); codeReview = c.labels.get("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 { 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()); exception.expect(AuthException.class); gApi.changes().id(triplet).current().review(ReviewInput.approve()); } @Test public void noteDbCommitsOnPatchSetCreation() throws Exception { assume().that(notesMigration.readChanges()).isTrue(); PushOneCommit.Result r = createChange(); pushFactory .create( db, admin.getIdent(), 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(new Change.Id(c._number))).getObjectId()); assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2"); PersonIdent expectedAuthor = changeNoteUtil.newIdent( accountCache.get(admin.id).getAccount(), c.updated, serverIdent.get(), AnonymousCowardNameProvider.DEFAULT); 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.newIdent( accountCache.get(admin.id).getAccount(), c.created, serverIdent.get(), AnonymousCowardNameProvider.DEFAULT); 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(info.branch).isEqualTo(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; exception.expect(ResourceConflictException.class); gApi.changes().create(in).get(); } @Test public void createNewPatchSetWithoutPermission() throws Exception { // Create new project with clean permissions Project.NameKey p = createProject("addPatchSet1"); // Clone separate repositories of the same project as admin and as user TestRepository adminTestRepo = cloneProject(p, admin); TestRepository userTestRepo = cloneProject(p, user); // Block default permission block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS); // Create change as admin PushOneCommit push = pushFactory.create(db, admin.getIdent(), adminTestRepo); PushOneCommit.Result r1 = push.to("refs/for/master"); r1.assertOkStatus(); // Fetch change GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":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().id + "."); } @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(db, admin.getIdent(), adminTestRepo); PushOneCommit.Result r1 = push.to("refs/for/master"); r1.assertOkStatus(); // Fetch change GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":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 = createProject("addPatchSet2"); // Clone separate repositories of the same project as admin and as user TestRepository adminTestRepo = cloneProject(project, admin); // Block default permission block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS); // Create change as admin PushOneCommit push = pushFactory.create(db, admin.getIdent(), adminTestRepo); PushOneCommit.Result r1 = push.to("refs/for/master"); r1.assertOkStatus(); // Fetch change GitUtil.fetch(adminTestRepo, r1.getPatchSet().getRefName() + ":ps"); adminTestRepo.reset("ps"); // Amend change as admin PushOneCommit.Result r2 = amendChange(r1.getChangeId(), "refs/for/master", admin, adminTestRepo); r2.assertOkStatus(); } @Test public void createMergePatchSet() throws Exception { PushOneCommit.Result start = pushTo("refs/heads/master"); start.assertOkStatus(); // create a change for master PushOneCommit.Result r = createChange(); r.assertOkStatus(); String changeId = r.getChangeId(); testRepo.reset(start.getCommit()); PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); currentMaster.assertOkStatus(); String parent = currentMaster.getCommit().getName(); // push a commit into dev branch createBranch(new Branch.NameKey(project, "dev")); PushOneCommit.Result changeA = pushFactory .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content") .to("refs/heads/dev"); changeA.assertOkStatus(); MergeInput mergeInput = new MergeInput(); mergeInput.source = "dev"; MergePatchSetInput in = new MergePatchSetInput(); in.merge = mergeInput; in.subject = "update change by merge ps2"; gApi.changes().id(changeId).createMergePatchSet(in); ChangeInfo changeInfo = gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); assertThat(changeInfo.revisions).hasSize(2); assertThat(changeInfo.subject).isEqualTo(in.subject); assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) .isEqualTo(parent); } @Test public void createMergePatchSetInheritParent() throws Exception { PushOneCommit.Result start = pushTo("refs/heads/master"); start.assertOkStatus(); // create a change for master PushOneCommit.Result r = createChange(); r.assertOkStatus(); String changeId = r.getChangeId(); String parent = r.getCommit().getParent(0).getName(); // advance master branch testRepo.reset(start.getCommit()); PushOneCommit.Result currentMaster = pushTo("refs/heads/master"); currentMaster.assertOkStatus(); // push a commit into dev branch createBranch(new Branch.NameKey(project, "dev")); PushOneCommit.Result changeA = pushFactory .create(db, user.getIdent(), testRepo, "change A", "A.txt", "A content") .to("refs/heads/dev"); changeA.assertOkStatus(); MergeInput mergeInput = new MergeInput(); mergeInput.source = "dev"; MergePatchSetInput in = new MergePatchSetInput(); in.merge = mergeInput; in.subject = "update change by merge ps2 inherit parent of ps1"; in.inheritParent = true; gApi.changes().id(changeId).createMergePatchSet(in); ChangeInfo changeInfo = gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION); assertThat(changeInfo.revisions).hasSize(2); assertThat(changeInfo.subject).isEqualTo(in.subject); assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) .isEqualTo(parent); assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit) .isNotEqualTo(currentMaster.getCommit().getName()); } @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("Code-Review"); assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); // add new label and assert that it's returned for existing changes ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); LabelType verified = Util.verified(); cfg.getLabelSections().put(verified.getName(), verified); AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); String heads = RefNames.REFS_HEADS + "*"; Util.allow(cfg, Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads); saveProjectConfig(project, cfg); change = gApi.changes().id(r.getChangeId()).get(); assertThat(change.labels.keySet()).containsExactly("Code-Review", "Verified"); assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review", "Verified"); assertPermitted(change, "Code-Review", -2, -1, 0, 1, 2); assertPermitted(change, "Verified", -1, 0, 1); // 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())); // remove label and assert that it's no longer returned for existing // changes, even if there is an approval for it cfg.getLabelSections().remove(verified.getName()); Util.remove(cfg, Permission.forLabel(verified.getName()), registeredUsers, heads); saveProjectConfig(project, cfg); change = gApi.changes().id(r.getChangeId()).get(); assertThat(change.labels.keySet()).containsExactly("Code-Review"); assertThat(change.permittedLabels.keySet()).containsExactly("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("Code-Review"); assertThat(change.permittedLabels).isEmpty(); } @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(ChangeStatus.MERGED); assertThat(change.labels.keySet()).containsExactly("Code-Review"); assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); assertPermitted(change, "Code-Review", 2); // add new label and assert that it's returned for existing changes ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); LabelType verified = Util.verified(); cfg.getLabelSections().put(verified.getName(), verified); AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); String heads = RefNames.REFS_HEADS + "*"; Util.allow(cfg, Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads); saveProjectConfig(project, cfg); change = gApi.changes().id(r.getChangeId()).get(); assertThat(change.labels.keySet()).containsExactly("Code-Review", "Verified"); assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review", "Verified"); assertPermitted(change, "Code-Review", 2); assertPermitted(change, "Verified", 0, 1); // ignore the new label by Prolog submit rule and assert that the label is // no longer returned GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config"); testRepo.reset("config"); PushOneCommit push2 = pushFactory.create( db, admin.getIdent(), 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, "Code-Review", 2); assertPermitted(change, "Verified"); // add an approval on the new label and assert that the label is now // returned although it is ignored by the Prolog submit rule and hence not // included in the submit records 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("Code-Review", "Verified"); assertPermitted(change, "Code-Review", 2); assertPermitted(change, "Verified"); // remove label and assert that it's no longer returned for existing // changes, even if there is an approval for it cfg = projectCache.checkedGet(project).getConfig(); cfg.getLabelSections().remove(verified.getName()); Util.remove(cfg, Permission.forLabel(verified.getName()), registeredUsers, heads); saveProjectConfig(project, cfg); change = gApi.changes().id(r.getChangeId()).get(); assertThat(change.labels.keySet()).containsExactly("Code-Review"); assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); assertPermitted(change, "Code-Review", 2); } @Test public void checkLabelsForMergedChangeWithNonAuthorCodeReview() throws Exception { // Configure Non-Author-Code-Review RevCommit oldHead = getRemoteHead(); GitUtil.fetch(testRepo, RefNames.REFS_CONFIG + ":config"); testRepo.reset("config"); PushOneCommit push2 = pushFactory.create( db, admin.getIdent(), 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); // Allow user to approve ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(); String heads = RefNames.REFS_HEADS + "*"; Util.allow( cfg, Permission.forLabel(Util.codeReview().getName()), -2, 2, registeredUsers, heads); saveProjectConfig(project, cfg); PushOneCommit.Result r = createChange(); setApiUser(user); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); setApiUser(admin); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); ChangeInfo change = gApi.changes().id(r.getChangeId()).get(); assertThat(change.status).isEqualTo(ChangeStatus.MERGED); assertThat(change.labels.keySet()).containsExactly("Code-Review", "Non-Author-Code-Review"); assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review"); assertPermitted(change, "Code-Review", 0, 1, 2); } @Test public void checkLabelsForAutoClosedChange() throws Exception { PushOneCommit.Result r = createChange(); PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo); PushOneCommit.Result result = push.to("refs/heads/master"); result.assertOkStatus(); ChangeInfo change = gApi.changes().id(r.getChangeId()).get(); assertThat(change.status).isEqualTo(ChangeStatus.MERGED); assertThat(change.labels.keySet()).containsExactly("Code-Review"); assertPermitted(change, "Code-Review", 0, 1, 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("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); ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); Util.allow( cfg, Permission.forLabel("Code-Review"), minPermittedValue, maxPermittedValue, REGISTERED_USERS, heads); saveProjectConfig(project, cfg); c = gApi.changes().id(triplet).get(DETAILED_LABELS); codeReview = c.labels.get("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 { ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); Util.blockLabel(cfg, "Code-Review", REGISTERED_USERS, "refs/heads/*"); saveProjectConfig(project, cfg); 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("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 votes = gApi.changes().id(changeId).current().reviewer(admin.email).votes(); assertThat(votes.keySet()).containsExactly("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("Code-Review", 3); gApi.changes().id(changeId).current().review(input); Map votes = gApi.changes().id(changeId).current().reviewer(admin.email).votes(); if (!notesMigration.readChanges()) { assertThat(votes.keySet()).containsExactly("Code-Review"); assertThat(votes.values()).containsExactly((short) 0); } else { assertThat(votes).isEmpty(); } } @Test @GerritConfig(name = "change.strictLabels", value = "true") public void strictLabelWithInvalidLabel() throws Exception { String changeId = createChange().getChangeId(); ReviewInput in = new ReviewInput().label("Code-Style", 1); exception.expect(BadRequestException.class); exception.expectMessage("label \"Code-Style\" is not a configured label"); gApi.changes().id(changeId).current().review(in); } @Test @GerritConfig(name = "change.strictLabels", value = "true") public void strictLabelWithInvalidValue() throws Exception { String changeId = createChange().getChangeId(); ReviewInput in = new ReviewInput().label("Code-Review", 3); exception.expect(BadRequestException.class); exception.expectMessage("label \"Code-Review\": 3 is not a valid value"); gApi.changes().id(changeId).current().review(in); } @Test public void unresolvedCommentsBlocked() throws Exception { modifySubmitRules( "submit_rule(submit(R)) :- \n" + "gerrit:unresolved_comments_count(0), \n" + "!," + "gerrit:commit_author(A), \n" + "R = label('All-Comments-Resolved', ok(A)).\n" + "submit_rule(submit(R)) :- \n" + "gerrit:unresolved_comments_count(U), \n" + "U > 0," + "R = label('All-Comments-Resolved', need(_)). \n\n"); String oldHead = getRemoteHead().name(); PushOneCommit.Result result1 = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); testRepo.reset(oldHead); PushOneCommit.Result result2 = pushFactory.create(db, user.getIdent(), 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(); exception.expect(ResourceConflictException.class); exception.expectMessage("Failed to submit 1 change due to the following problems"); exception.expectMessage("needs All-Comments-Resolved"); gApi.changes().id(result2.getChangeId()).current().submit(); } @Test public void pureRevertFactBlocksSubmissionOfNonReverts() throws Exception { addPureRevertSubmitRule(); // Create a change that is not a revert of another change PushOneCommit.Result r1 = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); approve(r1.getChangeId()); exception.expect(ResourceConflictException.class); exception.expectMessage("Failed to submit 1 change due to the following problems"); exception.expectMessage("needs Is-Pure-Revert"); gApi.changes().id(r1.getChangeId()).current().submit(); } @Test public void pureRevertFactBlocksSubmissionOfNonPureReverts() throws Exception { PushOneCommit.Result r1 = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); merge(r1); addPureRevertSubmitRule(); // Create a revert and push a content change String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId; amendChange(revertId); approve(revertId); exception.expect(ResourceConflictException.class); exception.expectMessage("Failed to submit 1 change due to the following problems"); exception.expectMessage("needs Is-Pure-Revert"); gApi.changes().id(revertId).current().submit(); } @Test public void pureRevertFactAllowsSubmissionOfPureReverts() throws Exception { // Create a change that we can later revert PushOneCommit.Result r1 = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); merge(r1); addPureRevertSubmitRule(); // Create a revert and submit it String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId; approve(revertId); gApi.changes().id(revertId).current().submit(); } @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 (TestAccount acc : ImmutableList.of(admin, user)) { setApiUser(acc); String newMessage = "modified commit by " + acc.username + "\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. setApiUser(admin); 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 changeCommitMessageWithNoChangeIdFails() throws Exception { PushOneCommit.Result r = createChange(); assertThat(getCommitMessage(r.getChangeId())) .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); exception.expect(ResourceConflictException.class); exception.expectMessage("missing Change-Id footer"); gApi.changes().id(r.getChangeId()).setMessage("modified commit\n"); } @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"); exception.expect(ResourceConflictException.class); exception.expectMessage("wrong Change-Id footer"); gApi.changes() .id(r.getChangeId()) .setMessage("modified commit\n\nChange-Id: " + otherChange.getChangeId() + "\n"); } @Test public void changeCommitMessageWithoutPermissionFails() throws Exception { // Create new project with clean permissions Project.NameKey p = createProject("addPatchSetEdit"); TestRepository userTestRepo = cloneProject(p, user); // Block default permission block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS); // Create change as user PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo); PushOneCommit.Result r = push.to("refs/for/master"); r.assertOkStatus(); // Try to change the commit message exception.expect(AuthException.class); exception.expectMessage("modifying commit message not permitted"); gApi.changes().id(r.getChangeId()).setMessage("foo"); } @Test public void changeCommitMessageWithSameMessageFails() throws Exception { PushOneCommit.Result r = createChange(); assertThat(getCommitMessage(r.getChangeId())) .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); exception.expect(ResourceConflictException.class); exception.expectMessage("new and existing commit message are the same"); gApi.changes().id(r.getChangeId()).setMessage(getCommitMessage(r.getChangeId())); } @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(db, admin.getIdent(), 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 comments = Iterables.getOnlyElement(gApi.changes().id(id).comments().values()); assertThat(Iterables.getOnlyElement(comments).message).isEqualTo(ci.message); } @Test public void pureRevertReturnsTrueForPureRevert() throws Exception { PushOneCommit.Result r = createChange(); merge(r); String revertId = gApi.changes().id(r.getChangeId()).revert().get().id; // Without query parameter assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue(); // With query parameter assertThat( gApi.changes() .id(revertId) .pureRevert(getRemoteHead().toObjectId().name()) .isPureRevert) .isTrue(); } @Test public void pureRevertReturnsFalseOnContentChange() throws Exception { PushOneCommit.Result r1 = createChange(); merge(r1); // Create a revert and expect pureRevert to be true String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId; assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue(); // Create a new PS and expect pureRevert to be false PushOneCommit.Result result = amendChange(revertId); result.assertOkStatus(); assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isFalse(); } @Test public void pureRevertParameterTakesPrecedence() throws Exception { PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1"); merge(r1); String oldHead = getRemoteHead().toObjectId().name(); PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content2"); merge(r2); String revertId = gApi.changes().id(r2.getChangeId()).revert().get().changeId; assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue(); assertThat(gApi.changes().id(revertId).pureRevert(oldHead).isPureRevert).isFalse(); } @Test public void pureRevertReturnsFalseOnInvalidInput() throws Exception { PushOneCommit.Result r1 = createChange(); merge(r1); exception.expect(BadRequestException.class); exception.expectMessage("invalid object ID"); gApi.changes().id(createChange().getChangeId()).pureRevert("invalid id"); } @Test public void pureRevertReturnsTrueWithCleanRebase() throws Exception { PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1"); merge(r1); PushOneCommit.Result r2 = createChange("commit message", "b.txt", "content2"); merge(r2); String revertId = gApi.changes().id(r1.getChangeId()).revert().get().changeId; // Rebase revert onto HEAD gApi.changes().id(revertId).rebase(); // Check that pureRevert is true which implies that the commit can be rebased onto the original // commit. assertThat(gApi.changes().id(revertId).pureRevert().isPureRevert).isTrue(); } @Test public void pureRevertReturnsFalseWithRebaseConflict() throws Exception { // Create an initial commit to serve as claimed original PushOneCommit.Result r1 = createChange("commit message", "a.txt", "content1"); merge(r1); String claimedOriginal = getRemoteHead().toObjectId().name(); // Change contents of the file to provoke a conflict merge(createChange("commit message", "a.txt", "content2")); // Create a commit that we can revert PushOneCommit.Result r2 = createChange("commit message", "a.txt", "content3"); merge(r2); // Create a revert of r2 String revertR3Id = gApi.changes().id(r2.getChangeId()).revert().id(); // Assert that the change is a pure revert of it's 'revertOf' assertThat(gApi.changes().id(revertR3Id).pureRevert().isPureRevert).isTrue(); // Assert that the change is not a pure revert of claimedOriginal because pureRevert is trying // to rebase this on claimed original, which fails. PureRevertInfo pureRevert = gApi.changes().id(revertR3Id).pureRevert(claimedOriginal); assertThat(pureRevert.isPureRevert).isFalse(); } @Test public void pureRevertThrowsExceptionWhenChangeIsNotARevertAndNoIdProvided() throws Exception { exception.expect(BadRequestException.class); exception.expectMessage("no ID was provided and change isn't a revert"); gApi.changes().id(createChange().getChangeId()).pureRevert(); } @Test public void putTopicExceedLimitFails() throws Exception { String changeId = createChange().getChangeId(); String topic = Stream.generate(() -> "t").limit(2049).collect(joining()); exception.expect(BadRequestException.class); exception.expectMessage("topic length exceeds the limit"); gApi.changes().id(changeId).topic(topic); } @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"); } public void submittableAfterLosingPermissions(String label) throws Exception { String codeReviewLabel = "Code-Review"; ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS; Util.allow(cfg, Permission.forLabel(label), -1, +1, registered, "refs/heads/*"); Util.allow(cfg, Permission.forLabel(codeReviewLabel), -2, +2, registered, "refs/heads/*"); saveProjectConfig(cfg); setApiUser(user); 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(); setApiUser(admin); // Remove user's permission for 'Label'. Util.remove(cfg, Permission.forLabel(label), registered, "refs/heads/*"); // Update user's permitted range for 'Code-Review' to be -1...+1. Util.remove(cfg, Permission.forLabel(codeReviewLabel), registered, "refs/heads/*"); Util.allow(cfg, Permission.forLabel(codeReviewLabel), -1, +1, registered, "refs/heads/*"); saveProjectConfig(cfg); // Verify user's new permitted range. setApiUser(user); 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(); setApiUser(admin); gApi.changes().id(changeId).current().submit(); } 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 getReviewers(Collection r) { return Iterables.transform(r, a -> new Account.Id(a._accountId)); } private ChangeResource parseResource(PushOneCommit.Result r) throws Exception { return parseChangeResource(r.getChangeId()); } private Optional getReviewerState(String changeId, Account.Id accountId) throws Exception { ChangeInfo c = gApi.changes().id(changeId).get(DETAILED_LABELS); Set states = c.reviewers .entrySet() .stream() .filter(e -> e.getValue().stream().anyMatch(a -> a._accountId == accountId.get())) .map(e -> e.getKey()) .collect(toSet()); assertThat(states.size()).named(states.toString()).isAtMost(1); return states.stream().findFirst(); } private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception { try (BatchUpdate batchUpdate = batchUpdateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { 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 in database. change.setStatus(newStatus); // Change status in NoteDb. PatchSet.Id currentPatchSetId = change.currentPatchSetId(); ctx.getUpdate(currentPatchSetId).setStatus(newStatus); return true; } } private void addPureRevertSubmitRule() throws Exception { modifySubmitRules( "submit_rule(submit(R)) :- \n" + "gerrit:pure_revert(1), \n" + "!," + "gerrit:commit_author(A), \n" + "R = label('Is-Pure-Revert', ok(A)).\n" + "submit_rule(submit(R)) :- \n" + "gerrit:pure_revert(U), \n" + "U \\= 1," + "R = label('Is-Pure-Revert', need(_)). \n\n"); } private void modifySubmitRules(String newContent) throws Exception { try (Repository repo = repoManager.openRepository(project)) { TestRepository testRepo = new TestRepository<>((InMemoryRepository) repo); testRepo .branch(RefNames.REFS_CONFIG) .commit() .author(admin.getIdent()) .committer(admin.getIdent()) .add("rules.pl", newContent) .message("Modify rules.pl") .create(); } } @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( db, admin.getIdent(), 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 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 public void starUnstar() throws Exception { 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); changeIndexedCounter.assertReindexOf(change); gApi.accounts().self().unstarChange(triplet); change = info(triplet); assertThat(change.starred).isNull(); assertThat(change.stars).isNull(); changeIndexedCounter.assertReindexOf(change); } @Test public void ignore() throws Exception { TestAccount user2 = accountCreator.user2(); PushOneCommit.Result r = createChange(); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; gApi.changes().id(r.getChangeId()).addReviewer(in); in = new AddReviewerInput(); in.reviewer = user2.email; gApi.changes().id(r.getChangeId()).addReviewer(in); setApiUser(user); gApi.changes().id(r.getChangeId()).ignore(true); assertThat(gApi.changes().id(r.getChangeId()).ignored()).isTrue(); sender.clear(); setApiUser(admin); gApi.changes().id(r.getChangeId()).abandon(); List messages = sender.getMessages(); assertThat(messages).hasSize(1); assertThat(messages.get(0).rcpt()).containsExactly(user2.emailAddress); setApiUser(user); gApi.changes().id(r.getChangeId()).ignore(false); assertThat(gApi.changes().id(r.getChangeId()).ignored()).isFalse(); } @Test public void cannotIgnoreOwnChange() throws Exception { String changeId = createChange().getChangeId(); exception.expect(BadRequestException.class); exception.expectMessage("cannot ignore own change"); gApi.changes().id(changeId).ignore(true); } @Test public void cannotIgnoreStarredChange() throws Exception { String changeId = createChange().getChangeId(); setApiUser(user); gApi.accounts().self().starChange(changeId); assertThat(gApi.changes().id(changeId).get().starred).isTrue(); exception.expect(ResourceConflictException.class); exception.expectMessage( "The labels " + StarredChangesUtil.DEFAULT_LABEL + " and " + StarredChangesUtil.IGNORE_LABEL + " are mutually exclusive. Only one of them can be set."); gApi.changes().id(changeId).ignore(true); } @Test public void cannotStarIgnoredChange() throws Exception { String changeId = createChange().getChangeId(); setApiUser(user); gApi.changes().id(changeId).ignore(true); assertThat(gApi.changes().id(changeId).ignored()).isTrue(); exception.expect(ResourceConflictException.class); exception.expectMessage( "The labels " + StarredChangesUtil.DEFAULT_LABEL + " and " + StarredChangesUtil.IGNORE_LABEL + " are mutually exclusive. Only one of them can be set."); gApi.accounts().self().starChange(changeId); } @Test public void markAsReviewed() throws Exception { TestAccount user2 = accountCreator.user2(); PushOneCommit.Result r = createChange(); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; gApi.changes().id(r.getChangeId()).addReviewer(in); setApiUser(user); assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull(); gApi.changes().id(r.getChangeId()).markAsReviewed(true); assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isTrue(); setApiUser(user2); sender.clear(); amendChange(r.getChangeId()); setApiUser(user); assertThat(gApi.changes().id(r.getChangeId()).get().reviewed).isNull(); List messages = sender.getMessages(); assertThat(messages).hasSize(1); assertThat(messages.get(0).rcpt()).containsExactly(user.emailAddress); } @Test public void cannotSetUnreviewedLabelForPatchSetThatAlreadyHasReviewedLabel() throws Exception { String changeId = createChange().getChangeId(); setApiUser(user); gApi.changes().id(changeId).markAsReviewed(true); assertThat(gApi.changes().id(changeId).get().reviewed).isTrue(); exception.expect(BadRequestException.class); exception.expectMessage( "The labels " + StarredChangesUtil.REVIEWED_LABEL + "/" + 1 + " and " + StarredChangesUtil.UNREVIEWED_LABEL + "/" + 1 + " are mutually exclusive. Only one of them can be set."); gApi.accounts() .self() .setStars( changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.UNREVIEWED_LABEL + "/1"))); } @Test public void cannotSetReviewedLabelForPatchSetThatAlreadyHasUnreviewedLabel() throws Exception { String changeId = createChange().getChangeId(); setApiUser(user); gApi.changes().id(changeId).markAsReviewed(false); assertThat(gApi.changes().id(changeId).get().reviewed).isNull(); exception.expect(BadRequestException.class); exception.expectMessage( "The labels " + StarredChangesUtil.REVIEWED_LABEL + "/" + 1 + " and " + StarredChangesUtil.UNREVIEWED_LABEL + "/" + 1 + " are mutually exclusive. Only one of them can be set."); gApi.accounts() .self() .setStars( changeId, new StarsInput(ImmutableSet.of(StarredChangesUtil.REVIEWED_LABEL + "/1"))); } @Test public void setReviewedAndUnreviewedLabelsForDifferentPatchSets() throws Exception { String changeId = createChange().getChangeId(); setApiUser(user); gApi.changes().id(changeId).markAsReviewed(true); assertThat(gApi.changes().id(changeId).get().reviewed).isTrue(); amendChange(changeId); assertThat(gApi.changes().id(changeId).get().reviewed).isNull(); gApi.changes().id(changeId).markAsReviewed(false); assertThat(gApi.changes().id(changeId).get().reviewed).isNull(); assertThat(gApi.accounts().self().getStars(changeId)) .containsExactly( StarredChangesUtil.REVIEWED_LABEL + "/" + 1, StarredChangesUtil.UNREVIEWED_LABEL + "/" + 2); } @Test public void cannotSetInvalidLabel() throws Exception { String changeId = createChange().getChangeId(); // label cannot contain whitespace String invalidLabel = "invalid label"; exception.expect(BadRequestException.class); exception.expectMessage("invalid labels: " + invalidLabel); gApi.accounts().self().setStars(changeId, new StarsInput(ImmutableSet.of(invalidLabel))); } private static class ChangeIndexedCounter implements ChangeIndexedListener { private final AtomicLongMap countsByChange = AtomicLongMap.create(); @Override public void onChangeIndexed(String projectName, int id) { countsByChange.incrementAndGet(id); } @Override public void onChangeDeleted(int id) { countsByChange.incrementAndGet(id); } void clear() { countsByChange.clear(); } long getCount(ChangeInfo info) { return countsByChange.get(info._number); } void assertReindexOf(ChangeInfo info) { assertReindexOf(info, 1); } void assertReindexOf(ChangeInfo info, int expectedCount) { assertThat(getCount(info)).isEqualTo(expectedCount); assertThat(countsByChange).hasSize(1); clear(); } } }