diff options
Diffstat (limited to 'gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java')
-rw-r--r-- | gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java | 3971 |
1 files changed, 0 insertions, 3971 deletions
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java deleted file mode 100644 index ce79a7c259..0000000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java +++ /dev/null @@ -1,3971 +0,0 @@ -// 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<ChangeMessageModifier> changeMessageModifiers; - - @Inject private DynamicSet<ChangeIndexedListener> 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 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<InMemoryRepository> 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<InMemoryRepository> 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<InMemoryRepository> 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<InMemoryRepository> 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<InMemoryRepository> 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 toggle 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 toggle work in progress"); - 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<AccountInfo>, Collection<String>> 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(); - 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(); - 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(); - 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 - @TestProjectInput(cloneAs = "user") - public void reviewWithWorkInProgressChangeOwner() throws Exception { - PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo); - PushOneCommit.Result r = push.to("refs/for/master"); - r.assertOkStatus(); - assertThat(r.getChange().change().getOwner()).isEqualTo(user.id); - - setApiUser(user); - ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); - gApi.changes().id(r.getChangeId()).current().review(in); - ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); - assertThat(info.workInProgress).isTrue(); - } - - @Test - @TestProjectInput(cloneAs = "user") - public void reviewWithWithWorkInProgressAdmin() throws Exception { - PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo); - PushOneCommit.Result r = push.to("refs/for/master"); - r.assertOkStatus(); - assertThat(r.getChange().change().getOwner()).isEqualTo(user.id); - - setApiUser(admin); - ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); - gApi.changes().id(r.getChangeId()).current().review(in); - ChangeInfo info = gApi.changes().id(r.getChangeId()).get(); - assertThat(info.workInProgress).isTrue(); - } - - @Test - public void reviewWithWorkInProgressByNonOwnerReturnsError() throws Exception { - PushOneCommit.Result r = createChange(); - ReviewInput in = ReviewInput.noScore().setWorkInProgress(true); - setApiUser(user); - exception.expect(AuthException.class); - exception.expectMessage("not allowed to toggle work in progress"); - gApi.changes().id(r.getChangeId()).current().review(in); - } - - @Test - public void reviewWithReadyByNonOwnerReturnsError() throws Exception { - PushOneCommit.Result r = createChange(); - ReviewInput in = ReviewInput.noScore().setReady(true); - setApiUser(user); - exception.expect(AuthException.class); - exception.expectMessage("not allowed to toggle work in progress"); - gApi.changes().id(r.getChangeId()).current().review(in); - } - - @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<ChangeMessageInfo> 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<Message> 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<ReviewerState, Collection<AccountInfo>> result = - gApi.changes().id(r.getChangeId()).revert().get().reviewers; - assertThat(result).containsKey(ReviewerState.REVIEWER); - - List<Integer> reviewers = - result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList()); - if (notesMigration.readChanges()) { - assertThat(result).containsKey(ReviewerState.CC); - List<Integer> 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<PatchSetApproval> 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); - eventRecorder.assertChangeDeletedEvents(changeId, deleteAs.email); - } 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<AccountInfo> 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<Message> 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<InMemoryRepository> 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<AccountInfo> 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<Message> 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<InMemoryRepository> 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<InMemoryRepository> 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<Message> 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<AccountInfo> 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 listReviewers() throws Exception { - PushOneCommit.Result r = createChange(); - AddReviewerInput in = new AddReviewerInput(); - in.reviewer = user.email; - gApi.changes().id(r.getChangeId()).addReviewer(in); - assertThat(gApi.changes().id(r.getChangeId()).reviewers()).hasSize(1); - - TestAccount user1 = - accountCreator.create(name("user1"), name("user1") + "@example.com", "User 1"); - in.reviewer = user1.email; - in.state = ReviewerState.CC; - gApi.changes().id(r.getChangeId()).addReviewer(in); - assertThat(gApi.changes().id(r.getChangeId()).reviewers().stream().map(a -> a.username)) - .containsExactly(user.username, user1.username); - } - - @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<Message> 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<AccountInfo> 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<Message> 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<AccountInfo> 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<AccountInfo> 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<AccountInfo> 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<AccountInfo> 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<String, Short> 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<AccountInfo> 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<AccountInfo> reviewers = gApi.changes().id(changeId).get().reviewers.get(REVIEWER); - - assertThat(reviewers).hasSize(2); - Iterator<AccountInfo> reviewerIt = reviewers.iterator(); - assertThat(reviewerIt.next()._accountId).isEqualTo(admin.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 removeReviewerSelfFromMergedChangeNotPermitted() throws Exception { - PushOneCommit.Result r = createChange(); - String changeId = r.getChangeId(); - - setApiUser(user); - recommend(changeId); - - setApiUser(admin); - approve(changeId); - gApi.changes().id(changeId).revision(r.getCommit().name()).submit(); - - setApiUser(user); - exception.expect(AuthException.class); - exception.expectMessage("remove reviewer not permitted"); - gApi.changes().id(r.getChangeId()).reviewer("self").remove(); - } - - @Test - public void removeReviewerSelfFromAbandonedChangePermitted() throws Exception { - PushOneCommit.Result r = createChange(); - String changeId = r.getChangeId(); - - setApiUser(user); - recommend(changeId); - - setApiUser(admin); - gApi.changes().id(changeId).abandon(); - - setApiUser(user); - gApi.changes().id(r.getChangeId()).reviewer("self").remove(); - eventRecorder.assertReviewerDeletedEvents(changeId, user.email); - } - - @Test - public void removeOtherReviewerFromAbandonedChangeNotPermitted() throws Exception { - PushOneCommit.Result r = createChange(); - String changeId = r.getChangeId(); - - setApiUser(user); - recommend(changeId); - - setApiUser(admin); - approve(changeId); - gApi.changes().id(changeId).abandon(); - - 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<Message> 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<String, Short> 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 <user@example.com>\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<ChangeInfo> results = gApi.changes().query().get(); - assertThat(results.size()).isAtLeast(1); - List<Integer> ids = new ArrayList<>(results.size()); - for (int i = 0; i < results.size(); i++) { - ChangeInfo info = results.get(i); - if (i == 0) { - assertThat(info._number).isEqualTo(r.getChange().getId().get()); - } - assertThat(Change.Status.forChangeStatus(info.status).isOpen()).isTrue(); - ids.add(info._number); - } - assertThat(ids).contains(r.getChange().getId().get()); - } - - @Test - public void queryChangesNoResults() throws Exception { - createChange(); - assertThat(query("message:test")).isNotEmpty(); - assertThat(query("message:{" + getClass().getName() + "fhqwhgads}")).isEmpty(); - } - - @Test - public void queryChanges() throws Exception { - PushOneCommit.Result r1 = createChange(); - createChange(); - List<ChangeInfo> results = query("project:{" + project.get() + "} " + r1.getChangeId()); - assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r1.getChangeId()); - } - - @Test - public void queryChangesLimit() throws Exception { - createChange(); - PushOneCommit.Result r2 = createChange(); - List<ChangeInfo> results = gApi.changes().query().withLimit(1).get(); - assertThat(results).hasSize(1); - assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r2.getChangeId()); - } - - @Test - public void queryChangesStart() throws Exception { - PushOneCommit.Result r1 = createChange(); - createChange(); - List<ChangeInfo> results = - gApi.changes().query("project:{" + project.get() + "}").withStart(1).get(); - assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r1.getChangeId()); - } - - @Test - public void queryChangesNoOptions() throws Exception { - PushOneCommit.Result r = createChange(); - ChangeInfo result = Iterables.getOnlyElement(query(r.getChangeId())); - assertThat(result.labels).isNull(); - assertThat(result.messages).isNull(); - assertThat(result.revisions).isNull(); - assertThat(result.actions).isNull(); - } - - @Test - public void queryChangesOptions() throws Exception { - PushOneCommit.Result r = createChange(); - - ChangeInfo result = Iterables.getOnlyElement(gApi.changes().query(r.getChangeId()).get()); - assertThat(result.labels).isNull(); - assertThat(result.messages).isNull(); - assertThat(result.actions).isNull(); - assertThat(result.revisions).isNull(); - - result = - Iterables.getOnlyElement( - gApi.changes() - .query(r.getChangeId()) - .withOptions( - ALL_REVISIONS, CHANGE_ACTIONS, CURRENT_ACTIONS, DETAILED_LABELS, MESSAGES) - .get()); - assertThat(Iterables.getOnlyElement(result.labels.keySet())).isEqualTo("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<String> footers = - new ArrayList<>( - Arrays.asList( - actual.revisions.get(r2.getCommit().getName()).commitWithFooters.split("\\n"))); - // remove subject + blank line - footers.remove(0); - footers.remove(0); - - List<String> expectedFooters = - Arrays.asList( - "Change-Id: " + r2.getChangeId(), - "Reviewed-on: " + canonicalWebUrl.get() + r2.getChange().getId(), - "Reviewed-by: Administrator <admin@example.com>", - "Custom2: Administrator <admin@example.com>", - "Tested-by: Administrator <admin@example.com>"); - - assertThat(footers).containsExactlyElementsIn(expectedFooters); - } - - @Test - public void customCommitFooters() throws Exception { - PushOneCommit.Result change = createChange(); - 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<String> footers = - new ArrayList<>( - Arrays.asList( - actual.revisions.get(change.getCommit().getName()).commitWithFooters.split("\\n"))); - // remove subject + blank line - footers.remove(0); - footers.remove(0); - - List<String> expectedFooters = - Arrays.asList( - "Change-Id: " + change.getChangeId(), - "Reviewed-on: " + canonicalWebUrl.get() + 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<InMemoryRepository> adminTestRepo = cloneProject(p, admin); - TestRepository<InMemoryRepository> 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<String, Short> 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<String, Short> 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<InMemoryRepository> 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<CommentInfo> 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<Account.Id> getReviewers(Collection<AccountInfo> 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<ReviewerState> getReviewerState(String changeId, Account.Id accountId) - throws Exception { - ChangeInfo c = gApi.changes().id(changeId).get(DETAILED_LABELS); - Set<ReviewerState> states = - c.reviewers.entrySet().stream() - .filter(e -> e.getValue().stream().anyMatch(a -> a._accountId == accountId.get())) - .map(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<TrackingIdInfo> trackingIds = change.trackingIds; - assertThat(trackingIds).isNotNull(); - assertThat(trackingIds).hasSize(1); - assertThat(trackingIds.iterator().next().system).isEqualTo("JIRA"); - assertThat(trackingIds.iterator().next().id).isEqualTo("JRA001"); - } - - @Test - 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<Message> 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<Message> 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<Integer> 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(); - } - } -} |