summaryrefslogtreecommitdiffstats
path: root/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
diff options
context:
space:
mode:
Diffstat (limited to 'javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java')
-rw-r--r--javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java914
1 files changed, 914 insertions, 0 deletions
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
new file mode 100644
index 0000000000..d1f4d8420f
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -0,0 +1,914 @@
+// Copyright (C) 2016 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.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
+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.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.TestAccount;
+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.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
+import com.google.gerrit.extensions.client.ReviewerState;
+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.LabelInfo;
+import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.change.ReviewerAdder;
+import com.google.gerrit.testing.FakeEmailSender.Message;
+import com.google.gson.stream.JsonReader;
+import java.util.ArrayList;
+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 org.junit.Test;
+
+public class ChangeReviewersIT extends AbstractDaemonTest {
+ @Test
+ public void addGroupAsReviewer() throws Exception {
+ // Set up two groups, one that is too large too add as reviewer, and one
+ // that is too large to add without confirmation.
+ String largeGroup = createGroup("largeGroup");
+ String mediumGroup = createGroup("mediumGroup");
+
+ int largeGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS + 1;
+ int mediumGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
+ List<TestAccount> users = createAccounts(largeGroupSize, "addGroupAsReviewer");
+ List<String> largeGroupUsernames = new ArrayList<>(mediumGroupSize);
+ for (TestAccount u : users) {
+ largeGroupUsernames.add(u.username);
+ }
+ List<String> mediumGroupUsernames = largeGroupUsernames.subList(0, mediumGroupSize);
+ gApi.groups()
+ .id(largeGroup)
+ .addMembers(largeGroupUsernames.toArray(new String[largeGroupSize]));
+ gApi.groups()
+ .id(mediumGroup)
+ .addMembers(mediumGroupUsernames.toArray(new String[mediumGroupSize]));
+
+ // Attempt to add overly large group as reviewers.
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ AddReviewerResult result = addReviewer(changeId, largeGroup);
+ assertThat(result.input).isEqualTo(largeGroup);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error).contains("has too many members to add them all as reviewers");
+ assertThat(result.reviewers).isNull();
+
+ // Attempt to add medium group without confirmation.
+ result = addReviewer(changeId, mediumGroup);
+ assertThat(result.input).isEqualTo(mediumGroup);
+ assertThat(result.confirm).isTrue();
+ assertThat(result.error)
+ .contains("has " + mediumGroupSize + " members. Do you want to add them all as reviewers?");
+ assertThat(result.reviewers).isNull();
+
+ // Add medium group with confirmation.
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = mediumGroup;
+ in.confirmed = true;
+ result = addReviewer(changeId, in);
+ assertThat(result.input).isEqualTo(mediumGroup);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error).isNull();
+ assertThat(result.reviewers).hasSize(mediumGroupSize);
+
+ // Verify that group members were added as reviewers.
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ assertReviewers(c, REVIEWER, users.subList(0, mediumGroupSize));
+ }
+
+ @Test
+ public void addCcAccount() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = user.email;
+ in.state = CC;
+ AddReviewerResult result = addReviewer(changeId, in);
+
+ assertThat(result.input).isEqualTo(user.email);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error).isNull();
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertThat(result.reviewers).isNull();
+ assertThat(result.ccs).hasSize(1);
+ AccountInfo ai = result.ccs.get(0);
+ assertThat(ai._accountId).isEqualTo(user.id.get());
+ assertReviewers(c, CC, user);
+ } else {
+ assertThat(result.ccs).isNull();
+ assertThat(result.reviewers).hasSize(1);
+ AccountInfo ai = result.reviewers.get(0);
+ assertThat(ai._accountId).isEqualTo(user.id.get());
+ assertReviewers(c, REVIEWER, user);
+ }
+
+ // Verify email was sent to CCed account.
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(user.emailAddress);
+ if (notesMigration.readChanges()) {
+ assertThat(m.body()).contains(admin.fullName + " has uploaded this change for review.");
+ } else {
+ assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+ assertThat(m.body()).contains("I'd like you to do a code review.");
+ }
+ }
+
+ @Test
+ public void addCcGroup() throws Exception {
+ List<TestAccount> users = createAccounts(6, "addCcGroup");
+ List<String> usernames = new ArrayList<>(6);
+ for (TestAccount u : users) {
+ usernames.add(u.username);
+ }
+
+ List<TestAccount> firstUsers = users.subList(0, 3);
+ List<String> firstUsernames = usernames.subList(0, 3);
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = createGroup("cc1");
+ in.state = CC;
+ gApi.groups()
+ .id(in.reviewer)
+ .addMembers(firstUsernames.toArray(new String[firstUsernames.size()]));
+ AddReviewerResult result = addReviewer(changeId, in);
+
+ assertThat(result.input).isEqualTo(in.reviewer);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error).isNull();
+ if (notesMigration.readChanges()) {
+ assertThat(result.reviewers).isNull();
+ } else {
+ assertThat(result.ccs).isNull();
+ }
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, CC, firstUsers);
+ } else {
+ assertReviewers(c, REVIEWER, firstUsers);
+ assertReviewers(c, CC);
+ }
+
+ // Verify emails were sent to each of the group's accounts.
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ List<Address> expectedAddresses = new ArrayList<>(firstUsers.size());
+ for (TestAccount u : firstUsers) {
+ expectedAddresses.add(u.emailAddress);
+ }
+ assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
+
+ // CC a group that overlaps with some existing reviewers and CCed accounts.
+ TestAccount reviewer =
+ accountCreator.create(name("reviewer"), "addCcGroup-reviewer@example.com", "Reviewer");
+ result = addReviewer(changeId, reviewer.username);
+ assertThat(result.error).isNull();
+ sender.clear();
+ in.reviewer = createGroup("cc2");
+ gApi.groups().id(in.reviewer).addMembers(usernames.toArray(new String[usernames.size()]));
+ gApi.groups().id(in.reviewer).addMembers(reviewer.username);
+ result = addReviewer(changeId, in);
+ assertThat(result.input).isEqualTo(in.reviewer);
+ assertThat(result.confirm).isNull();
+ assertThat(result.error).isNull();
+ c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertThat(result.ccs).hasSize(3);
+ assertThat(result.reviewers).isNull();
+ assertReviewers(c, REVIEWER, reviewer);
+ assertReviewers(c, CC, users);
+ } else {
+ assertThat(result.ccs).isNull();
+ assertThat(result.reviewers).hasSize(3);
+ List<TestAccount> expectedUsers = new ArrayList<>(users.size() + 2);
+ expectedUsers.addAll(users);
+ expectedUsers.add(reviewer);
+ assertReviewers(c, REVIEWER, expectedUsers);
+ }
+
+ messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ m = messages.get(0);
+ expectedAddresses = new ArrayList<>(4);
+ for (int i = 0; i < 3; i++) {
+ expectedAddresses.add(users.get(users.size() - i - 1).emailAddress);
+ }
+ if (!notesMigration.readChanges()) {
+ for (int i = 0; i < 3; i++) {
+ expectedAddresses.add(users.get(i).emailAddress);
+ }
+ }
+ expectedAddresses.add(reviewer.emailAddress);
+ assertThat(m.rcpt()).containsExactlyElementsIn(expectedAddresses);
+ }
+
+ @Test
+ public void transitionCcToReviewer() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = user.email;
+ in.state = CC;
+ addReviewer(changeId, in);
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, REVIEWER);
+ assertReviewers(c, CC, user);
+ } else {
+ assertReviewers(c, REVIEWER, user);
+ assertReviewers(c, CC);
+ }
+
+ in.state = REVIEWER;
+ addReviewer(changeId, in);
+ c = gApi.changes().id(r.getChangeId()).get();
+ assertReviewers(c, REVIEWER, user);
+ assertReviewers(c, CC);
+ }
+
+ @Test
+ public void driveByComment() throws Exception {
+ // Create change owned by admin.
+ PushOneCommit.Result r = createChange();
+
+ // Post drive-by message as user.
+ ReviewInput input = new ReviewInput().message("hello");
+ RestResponse resp =
+ userRestSession.post(
+ "/changes/" + r.getChangeId() + "/revisions/" + r.getCommit().getName() + "/review",
+ input);
+ ReviewResult result = readContentFromJson(resp, 200, ReviewResult.class);
+ assertThat(result.labels).isNull();
+ assertThat(result.reviewers).isNull();
+
+ // Verify user is added to CC list.
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, REVIEWER);
+ assertReviewers(c, CC, user);
+ } else {
+ // If we aren't reading from NoteDb, the user will appear as a
+ // reviewer.
+ assertReviewers(c, REVIEWER, user);
+ assertReviewers(c, CC);
+ }
+ }
+
+ @Test
+ public void addSelfAsReviewer() throws Exception {
+ // Create change owned by admin.
+ PushOneCommit.Result r = createChange();
+
+ // user adds self as REVIEWER.
+ ReviewInput input = new ReviewInput().reviewer(user.username);
+ RestResponse resp =
+ userRestSession.post(
+ "/changes/" + r.getChangeId() + "/revisions/" + r.getCommit().getName() + "/review",
+ input);
+ ReviewResult result = readContentFromJson(resp, 200, ReviewResult.class);
+ assertThat(result.labels).isNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(1);
+
+ // Verify reviewer state.
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ assertReviewers(c, REVIEWER, user);
+ assertReviewers(c, CC);
+ LabelInfo label = c.labels.get("Code-Review");
+ assertThat(label).isNotNull();
+ assertThat(label.all).isNotNull();
+ assertThat(label.all).hasSize(1);
+ ApprovalInfo approval = label.all.get(0);
+ assertThat(approval._accountId).isEqualTo(user.getId().get());
+ }
+
+ @Test
+ public void addSelfAsCc() throws Exception {
+ // Create change owned by admin.
+ PushOneCommit.Result r = createChange();
+
+ // user adds self as CC.
+ ReviewInput input = new ReviewInput().reviewer(user.username, CC, false);
+ RestResponse resp =
+ userRestSession.post(
+ "/changes/" + r.getChangeId() + "/revisions/" + r.getCommit().getName() + "/review",
+ input);
+ ReviewResult result = readContentFromJson(resp, 200, ReviewResult.class);
+ assertThat(result.labels).isNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(1);
+
+ // Verify reviewer state.
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, REVIEWER);
+ assertReviewers(c, CC, user);
+ // Verify no approvals were added.
+ assertThat(c.labels).isNotNull();
+ LabelInfo label = c.labels.get("Code-Review");
+ assertThat(label).isNotNull();
+ assertThat(label.all).isNull();
+ } else {
+ // When approvals are stored in ReviewDb, we still create a label for
+ // the reviewing user, and force them into the REVIEWER state.
+ assertReviewers(c, REVIEWER, user);
+ assertReviewers(c, CC);
+ LabelInfo label = c.labels.get("Code-Review");
+ assertThat(label).isNotNull();
+ assertThat(label.all).isNotNull();
+ assertThat(label.all).hasSize(1);
+ ApprovalInfo approval = label.all.get(0);
+ assertThat(approval._accountId).isEqualTo(user.getId().get());
+ }
+ }
+
+ @Test
+ public void reviewerReplyWithoutVote() throws Exception {
+ // Create change owned by admin.
+ PushOneCommit.Result r = createChange();
+
+ // Verify reviewer state.
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ assertReviewers(c, REVIEWER);
+ assertReviewers(c, CC);
+ LabelInfo label = c.labels.get("Code-Review");
+ assertThat(label).isNotNull();
+ assertThat(label.all).isNull();
+
+ // Add user as REVIEWER.
+ ReviewInput input = new ReviewInput().reviewer(user.username);
+ ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.labels).isNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(1);
+
+ // Verify reviewer state. Both admin and user should be REVIEWERs now,
+ // because admin gets forced into REVIEWER state by virtue of being owner.
+ c = gApi.changes().id(r.getChangeId()).get();
+ assertReviewers(c, REVIEWER, admin, user);
+ assertReviewers(c, CC);
+ label = c.labels.get("Code-Review");
+ assertThat(label).isNotNull();
+ assertThat(label.all).isNotNull();
+ assertThat(label.all).hasSize(2);
+ Map<Integer, Integer> approvals = new HashMap<>();
+ for (ApprovalInfo approval : label.all) {
+ approvals.put(approval._accountId, approval.value);
+ }
+ assertThat(approvals).containsEntry(admin.getId().get(), 0);
+ assertThat(approvals).containsEntry(user.getId().get(), 0);
+
+ // Comment as user without voting. This should delete the approval and
+ // then replace it with the default value.
+ input = new ReviewInput().message("hello");
+ RestResponse resp =
+ userRestSession.post(
+ "/changes/" + r.getChangeId() + "/revisions/" + r.getCommit().getName() + "/review",
+ input);
+ result = readContentFromJson(resp, 200, ReviewResult.class);
+ assertThat(result.labels).isNull();
+
+ // Verify reviewer state.
+ c = gApi.changes().id(r.getChangeId()).get();
+ assertReviewers(c, REVIEWER, admin, user);
+ assertReviewers(c, CC);
+ label = c.labels.get("Code-Review");
+ assertThat(label).isNotNull();
+ assertThat(label.all).isNotNull();
+ assertThat(label.all).hasSize(2);
+ approvals.clear();
+ for (ApprovalInfo approval : label.all) {
+ approvals.put(approval._accountId, approval.value);
+ }
+ assertThat(approvals).containsEntry(admin.getId().get(), 0);
+ assertThat(approvals).containsEntry(user.getId().get(), 0);
+ }
+
+ @Test
+ public void reviewAndAddReviewers() throws Exception {
+ TestAccount observer = accountCreator.user2();
+ PushOneCommit.Result r = createChange();
+ ReviewInput input =
+ ReviewInput.approve().reviewer(user.email).reviewer(observer.email, CC, false);
+
+ ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.labels).isNotNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(2);
+
+ // Verify reviewer and CC were added. If not in NoteDb read mode, both
+ // parties will be returned as CCed.
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, REVIEWER, admin, user);
+ assertReviewers(c, CC, observer);
+ } else {
+ // In legacy mode, everyone should be a reviewer.
+ assertReviewers(c, REVIEWER, admin, user, observer);
+ assertReviewers(c, CC);
+ }
+
+ // Verify emails were sent to added reviewers.
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(2);
+
+ Message m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
+ assertThat(m.body()).contains(admin.fullName + " has posted comments on this change.");
+ assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+ assertThat(m.body()).contains("Patch Set 1: Code-Review+2");
+
+ m = messages.get(1);
+ assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
+ assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+ assertThat(m.body()).contains("I'd like you to do a code review.");
+ }
+
+ @Test
+ public void reviewAndAddGroupReviewers() throws Exception {
+ int largeGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS + 1;
+ int mediumGroupSize = ReviewerAdder.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK + 1;
+ List<TestAccount> users = createAccounts(largeGroupSize, "reviewAndAddGroupReviewers");
+ List<String> usernames = new ArrayList<>(largeGroupSize);
+ for (TestAccount u : users) {
+ usernames.add(u.username);
+ }
+
+ String largeGroup = createGroup("largeGroup");
+ String mediumGroup = createGroup("mediumGroup");
+ gApi.groups().id(largeGroup).addMembers(usernames.toArray(new String[largeGroupSize]));
+ gApi.groups()
+ .id(mediumGroup)
+ .addMembers(usernames.subList(0, mediumGroupSize).toArray(new String[mediumGroupSize]));
+
+ TestAccount observer = accountCreator.user2();
+ PushOneCommit.Result r = createChange();
+
+ // Attempt to add overly large group as reviewers.
+ ReviewInput input =
+ ReviewInput.approve()
+ .reviewer(user.email)
+ .reviewer(observer.email, CC, false)
+ .reviewer(largeGroup);
+ ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input, SC_BAD_REQUEST);
+ assertThat(result.labels).isNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(3);
+ AddReviewerResult reviewerResult = result.reviewers.get(largeGroup);
+ assertThat(reviewerResult).isNotNull();
+ assertThat(reviewerResult.confirm).isNull();
+ assertThat(reviewerResult.error).isNotNull();
+ assertThat(reviewerResult.error).contains("has too many members to add them all as reviewers");
+
+ // No labels should have changed, and no reviewers/CCs should have been added.
+ ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
+ assertThat(c.messages).hasSize(1);
+ assertThat(c.reviewers.get(REVIEWER)).isNull();
+ assertThat(c.reviewers.get(CC)).isNull();
+
+ // Attempt to add group large enough to require confirmation, without
+ // confirmation, as reviewers.
+ input =
+ ReviewInput.approve()
+ .reviewer(user.email)
+ .reviewer(observer.email, CC, false)
+ .reviewer(mediumGroup);
+ result = review(r.getChangeId(), r.getCommit().name(), input, SC_BAD_REQUEST);
+ assertThat(result.labels).isNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(3);
+ reviewerResult = result.reviewers.get(mediumGroup);
+ assertThat(reviewerResult).isNotNull();
+ assertThat(reviewerResult.confirm).isTrue();
+ assertThat(reviewerResult.error)
+ .contains("has " + mediumGroupSize + " members. Do you want to add them all as reviewers?");
+
+ // No labels should have changed, and no reviewers/CCs should have been added.
+ c = gApi.changes().id(r.getChangeId()).get();
+ assertThat(c.messages).hasSize(1);
+ assertThat(c.reviewers.get(REVIEWER)).isNull();
+ assertThat(c.reviewers.get(CC)).isNull();
+
+ // Retrying with confirmation should successfully approve and add reviewers/CCs.
+ input = ReviewInput.approve().reviewer(user.email).reviewer(mediumGroup, CC, true);
+ result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.labels).isNotNull();
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(2);
+
+ c = gApi.changes().id(r.getChangeId()).get();
+ assertThat(c.messages).hasSize(2);
+
+ if (notesMigration.readChanges()) {
+ assertReviewers(c, REVIEWER, admin, user);
+ assertReviewers(c, CC, users.subList(0, mediumGroupSize));
+ } else {
+ // If not in NoteDb mode, then everyone is a REVIEWER.
+ List<TestAccount> expected = users.subList(0, mediumGroupSize);
+ expected.add(admin);
+ expected.add(user);
+ assertReviewers(c, REVIEWER, expected);
+ assertReviewers(c, CC);
+ }
+ }
+
+ @Test
+ public void noteDbAddReviewerToReviewerChangeInfo() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = user.email;
+ in.state = CC;
+ addReviewer(changeId, in);
+
+ in.state = REVIEWER;
+ addReviewer(changeId, in);
+
+ gApi.changes().id(changeId).current().review(ReviewInput.dislike());
+
+ setApiUser(user);
+ // NoteDb adds reviewer to a change on every review.
+ gApi.changes().id(changeId).current().review(ReviewInput.dislike());
+
+ deleteReviewer(changeId, user).assertNoContent();
+
+ ChangeInfo c = gApi.changes().id(changeId).get();
+ assertThat(c.reviewerUpdates).isNotNull();
+ assertThat(c.reviewerUpdates).hasSize(3);
+
+ Iterator<ReviewerUpdateInfo> it = c.reviewerUpdates.iterator();
+ ReviewerUpdateInfo reviewerChange = it.next();
+ assertThat(reviewerChange.state).isEqualTo(CC);
+ assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.getId().get());
+ assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.getId().get());
+
+ reviewerChange = it.next();
+ assertThat(reviewerChange.state).isEqualTo(REVIEWER);
+ assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.getId().get());
+ assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.getId().get());
+
+ reviewerChange = it.next();
+ assertThat(reviewerChange.state).isEqualTo(REMOVED);
+ assertThat(reviewerChange.reviewer._accountId).isEqualTo(user.getId().get());
+ assertThat(reviewerChange.updatedBy._accountId).isEqualTo(admin.getId().get());
+ }
+
+ @Test
+ public void addDuplicateReviewers() throws Exception {
+ PushOneCommit.Result r = createChange();
+ ReviewInput input = ReviewInput.approve().reviewer(user.email).reviewer(user.email);
+ ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(1);
+ AddReviewerResult reviewerResult = result.reviewers.get(user.email);
+ assertThat(reviewerResult).isNotNull();
+ assertThat(reviewerResult.confirm).isNull();
+ assertThat(reviewerResult.error).isNull();
+ }
+
+ @Test
+ public void addOverlappingGroups() throws Exception {
+ String emailPrefix = "addOverlappingGroups-";
+ TestAccount user1 =
+ accountCreator.create(name("user1"), emailPrefix + "user1@example.com", "User1");
+ TestAccount user2 =
+ accountCreator.create(name("user2"), emailPrefix + "user2@example.com", "User2");
+ TestAccount user3 =
+ accountCreator.create(name("user3"), emailPrefix + "user3@example.com", "User3");
+ String group1 = createGroup("group1");
+ String group2 = createGroup("group2");
+ gApi.groups().id(group1).addMembers(user1.username, user2.username);
+ gApi.groups().id(group2).addMembers(user2.username, user3.username);
+
+ PushOneCommit.Result r = createChange();
+ ReviewInput input = ReviewInput.approve().reviewer(group1).reviewer(group2);
+ ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(2);
+ AddReviewerResult reviewerResult = result.reviewers.get(group1);
+ assertThat(reviewerResult.error).isNull();
+ assertThat(reviewerResult.reviewers).hasSize(2);
+ reviewerResult = result.reviewers.get(group2);
+ assertThat(reviewerResult.error).isNull();
+ assertThat(reviewerResult.reviewers).hasSize(1);
+
+ // Repeat the above for CCs
+ if (!notesMigration.readChanges()) {
+ return;
+ }
+ r = createChange();
+ input = ReviewInput.approve().reviewer(group1, CC, false).reviewer(group2, CC, false);
+ result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(2);
+ reviewerResult = result.reviewers.get(group1);
+ assertThat(reviewerResult.error).isNull();
+ assertThat(reviewerResult.ccs).hasSize(2);
+ reviewerResult = result.reviewers.get(group2);
+ assertThat(reviewerResult.error).isNull();
+ assertThat(reviewerResult.ccs).hasSize(1);
+
+ // Repeat again with one group REVIEWER, the other CC. The overlapping
+ // member should end up as a REVIEWER.
+ r = createChange();
+ input = ReviewInput.approve().reviewer(group1, REVIEWER, false).reviewer(group2, CC, false);
+ result = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(result.reviewers).isNotNull();
+ assertThat(result.reviewers).hasSize(2);
+ reviewerResult = result.reviewers.get(group1);
+ assertThat(reviewerResult.error).isNull();
+ assertThat(reviewerResult.reviewers).hasSize(2);
+ reviewerResult = result.reviewers.get(group2);
+ assertThat(reviewerResult.error).isNull();
+ assertThat(reviewerResult.reviewers).isNull();
+ assertThat(reviewerResult.ccs).hasSize(1);
+ }
+
+ @Test
+ public void removingReviewerRemovesTheirVote() throws Exception {
+ String crLabel = "Code-Review";
+ PushOneCommit.Result r = createChange();
+ ReviewInput input = ReviewInput.approve().reviewer(admin.email);
+ ReviewResult addResult = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(addResult.reviewers).isNotNull();
+ assertThat(addResult.reviewers).hasSize(1);
+
+ Map<String, LabelInfo> changeLabels = getChangeLabels(r.getChangeId());
+ assertThat(changeLabels.get(crLabel).all).hasSize(1);
+
+ RestResponse deleteResult = deleteReviewer(r.getChangeId(), admin);
+ deleteResult.assertNoContent();
+
+ changeLabels = getChangeLabels(r.getChangeId());
+ assertThat(changeLabels.get(crLabel).all).isNull();
+
+ // Check that the vote is gone even after the reviewer is added back
+ addReviewer(r.getChangeId(), admin.email);
+ changeLabels = getChangeLabels(r.getChangeId());
+ assertThat(changeLabels.get(crLabel).all).isNull();
+ }
+
+ @Test
+ public void notifyDetailsWorkOnPostReview() throws Exception {
+ PushOneCommit.Result r = createChange();
+ TestAccount userToNotify = createAccounts(1, "notify-details-post-review").get(0);
+
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.reviewer(user.email, ReviewerState.REVIEWER, true);
+ reviewInput.notify = NotifyHandling.NONE;
+ reviewInput.notifyDetails =
+ ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+
+ sender.clear();
+ gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ assertThat(sender.getMessages()).hasSize(1);
+ assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress);
+ }
+
+ @Test
+ public void notifyDetailsWorkOnPostReviewers() throws Exception {
+ PushOneCommit.Result r = createChange();
+ TestAccount userToNotify = createAccounts(1, "notify-details-post-reviewers").get(0);
+
+ AddReviewerInput addReviewer = new AddReviewerInput();
+ addReviewer.reviewer = user.email;
+ addReviewer.notify = NotifyHandling.NONE;
+ addReviewer.notifyDetails =
+ ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+
+ sender.clear();
+ gApi.changes().id(r.getChangeId()).addReviewer(addReviewer);
+ assertThat(sender.getMessages()).hasSize(1);
+ assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress);
+ }
+
+ @Test
+ public void removeReviewerWithVoteWithoutPermissionFails() throws Exception {
+ PushOneCommit.Result r = createChange();
+ TestAccount newUser = createAccounts(1, name("foo")).get(0);
+
+ setApiUser(user);
+ gApi.changes().id(r.getChangeId()).current().review(new ReviewInput().label("Code-Review", 1));
+ setApiUser(newUser);
+ exception.expect(AuthException.class);
+ exception.expectMessage("remove reviewer not permitted");
+ gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+ }
+
+ @Test
+ @Sandboxed
+ public void removeReviewerWithoutVoteWithPermissionSucceeds() throws Exception {
+ PushOneCommit.Result r = createChange();
+ // This test creates a new user so that it can explicitly check the REMOVE_REVIEWER permission
+ // rather than bypassing the check because of project or ref ownership.
+ TestAccount newUser = createAccounts(1, name("foo")).get(0);
+ grant(project, RefNames.REFS + "*", Permission.REMOVE_REVIEWER, false, REGISTERED_USERS);
+
+ gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+ assertThatUserIsOnlyReviewer(r.getChangeId());
+ setApiUser(newUser);
+ gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+ assertThat(gApi.changes().id(r.getChangeId()).get().reviewers).isEmpty();
+ }
+
+ @Test
+ public void removeReviewerWithoutVoteWithoutPermissionFails() throws Exception {
+ PushOneCommit.Result r = createChange();
+ TestAccount newUser = createAccounts(1, name("foo")).get(0);
+
+ gApi.changes().id(r.getChangeId()).addReviewer(user.email);
+ setApiUser(newUser);
+ exception.expect(AuthException.class);
+ exception.expectMessage("remove reviewer not permitted");
+ gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+ }
+
+ @Test
+ public void removeCCWithoutPermissionFails() throws Exception {
+ PushOneCommit.Result r = createChange();
+ TestAccount newUser = createAccounts(1, name("foo")).get(0);
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = user.email;
+ input.state = ReviewerState.CC;
+ gApi.changes().id(r.getChangeId()).addReviewer(input);
+ setApiUser(newUser);
+ exception.expect(AuthException.class);
+ exception.expectMessage("remove reviewer not permitted");
+ gApi.changes().id(r.getChangeId()).reviewer(user.email).remove();
+ }
+
+ @Test
+ public void addExistingReviewerShortCircuits() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = user.email;
+ input.state = ReviewerState.REVIEWER;
+
+ AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
+ assertThat(result.reviewers).hasSize(1);
+ ReviewerInfo info = result.reviewers.get(0);
+ assertThat(info._accountId).isEqualTo(user.id.get());
+
+ assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).reviewers).isEmpty();
+ }
+
+ @Test
+ public void addExistingCcShortCircuits() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = user.email;
+ input.state = ReviewerState.CC;
+
+ AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(input);
+ assertThat(result.ccs).hasSize(1);
+ AccountInfo info = result.ccs.get(0);
+ assertThat(info._accountId).isEqualTo(user.id.get());
+
+ assertThat(gApi.changes().id(r.getChangeId()).addReviewer(input).ccs).isEmpty();
+ }
+
+ private void assertThatUserIsOnlyReviewer(String changeId) throws Exception {
+ AccountInfo userInfo = new AccountInfo(user.fullName, user.emailAddress.getEmail());
+ userInfo._accountId = user.id.get();
+ userInfo.username = user.username;
+ assertThat(gApi.changes().id(changeId).get().reviewers)
+ .containsExactly(ReviewerState.REVIEWER, ImmutableList.of(userInfo));
+ }
+
+ private AddReviewerResult addReviewer(String changeId, String reviewer) throws Exception {
+ return addReviewer(changeId, reviewer, SC_OK);
+ }
+
+ private AddReviewerResult addReviewer(String changeId, String reviewer, int expectedStatus)
+ throws Exception {
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = reviewer;
+ return addReviewer(changeId, in, expectedStatus);
+ }
+
+ private AddReviewerResult addReviewer(String changeId, AddReviewerInput in) throws Exception {
+ return addReviewer(changeId, in, SC_OK);
+ }
+
+ private AddReviewerResult addReviewer(String changeId, AddReviewerInput in, int expectedStatus)
+ throws Exception {
+ RestResponse resp = adminRestSession.post("/changes/" + changeId + "/reviewers", in);
+ return readContentFromJson(resp, expectedStatus, AddReviewerResult.class);
+ }
+
+ private RestResponse deleteReviewer(String changeId, TestAccount account) throws Exception {
+ return adminRestSession.delete("/changes/" + changeId + "/reviewers/" + account.getId().get());
+ }
+
+ private ReviewResult review(String changeId, String revisionId, ReviewInput in) throws Exception {
+ return review(changeId, revisionId, in, SC_OK);
+ }
+
+ private ReviewResult review(
+ String changeId, String revisionId, ReviewInput in, int expectedStatus) throws Exception {
+ RestResponse resp =
+ adminRestSession.post("/changes/" + changeId + "/revisions/" + revisionId + "/review", in);
+ return readContentFromJson(resp, expectedStatus, ReviewResult.class);
+ }
+
+ private static <T> T readContentFromJson(RestResponse r, int expectedStatus, Class<T> clazz)
+ throws Exception {
+ r.assertStatus(expectedStatus);
+ try (JsonReader jsonReader = new JsonReader(r.getReader())) {
+ jsonReader.setLenient(true);
+ return newGson().fromJson(jsonReader, clazz);
+ }
+ }
+
+ private static void assertReviewers(
+ ChangeInfo c, ReviewerState reviewerState, TestAccount... accounts) throws Exception {
+ List<TestAccount> accountList = new ArrayList<>(accounts.length);
+ Collections.addAll(accountList, accounts);
+ assertReviewers(c, reviewerState, accountList);
+ }
+
+ private static void assertReviewers(
+ ChangeInfo c, ReviewerState reviewerState, Iterable<TestAccount> accounts) throws Exception {
+ Collection<AccountInfo> actualAccounts = c.reviewers.get(reviewerState);
+ if (actualAccounts == null) {
+ assertThat(accounts.iterator().hasNext()).isFalse();
+ return;
+ }
+ assertThat(actualAccounts).isNotNull();
+ List<Integer> actualAccountIds = new ArrayList<>(actualAccounts.size());
+ for (AccountInfo account : actualAccounts) {
+ actualAccountIds.add(account._accountId);
+ }
+ List<Integer> expectedAccountIds = new ArrayList<>();
+ for (TestAccount account : accounts) {
+ expectedAccountIds.add(account.getId().get());
+ }
+ assertThat(actualAccountIds).containsExactlyElementsIn(expectedAccountIds);
+ }
+
+ private List<TestAccount> createAccounts(int n, String emailPrefix) throws Exception {
+ List<TestAccount> result = new ArrayList<>(n);
+ for (int i = 0; i < n; i++) {
+ result.add(
+ accountCreator.create(
+ name("u" + i), emailPrefix + "-" + i + "@example.com", "Full Name " + i));
+ }
+ return result;
+ }
+
+ private Map<String, LabelInfo> getChangeLabels(String changeId) throws Exception {
+ return gApi.changes().id(changeId).get(DETAILED_LABELS).labels;
+ }
+}