PostReview: Do not fail with ISE if reviewer in ReviewerInput is missing

Before this change, calling PostReview failed with '500 Internal Server
Error' when the user provided a ReviewerInput that didn't have the
'reviewer' field set.

If bad user input is provided we usually reject the request with '400
Bad Request' but since PostReview can batch multiple operations (e.g.
multiple reviewer updates) it always returns '200 OK' and errors are
provided in the returned ReviewResult. Hence if a ReviewerInput without
a reviewer is specified return a proper error message in the
ReviewResult.

Adding reviewers is also possible with PostReviewers. While we are at
this update PostReviewers to make the error handling for missing
reviewer user identifiers consistent and improve the error message:

* Before this change, PostReviewers rejected a null reviewer with a '400
  Bad Request' reposnse but an empty reviewer resulted in a '200 OK'
  response with an error in the returned ReviewerResult. Now in both
  cases we return a '200 OK' response with an error in the returned
  ReviewerResult.

* The error message for when an empty reviewer is provided is improved
  from saying " is not a valid user identifier" to saying "reviewer user
  identifier is required".

Bug: Google b/326096919
Release-Notes: Fixed internal server error when posting a review with a ReviewerInput that didn't set the 'reviewer' field.
Change-Id: I0b523c58a97a7c48d5d7610ea24d167a5f8fa0b9
Signed-off-by: Edwin Kempin <ekempin@google.com>
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index d714215..4b1d06c 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -24,6 +24,7 @@
 import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
 
+import autovalue.shaded.com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -222,7 +223,10 @@
       throws IOException, PermissionBackendException, ConfigInvalidException {
     try (TraceContext.TraceTimer ignored =
         TraceContext.newTimer(getClass().getSimpleName() + "#prepare", Metadata.empty())) {
-      requireNonNull(input.reviewer);
+      if (Strings.nullToEmpty(input.reviewer).trim().isEmpty()) {
+        return fail(input, FailureType.NOT_FOUND, "reviewer user identifier is required");
+      }
+
       boolean confirmed = input.confirmed();
       boolean allowByEmail =
           projectCache
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index b0e58c5..675610d 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -65,10 +65,6 @@
   public Response<ReviewerResult> apply(ChangeResource rsrc, ReviewerInput input)
       throws IOException, RestApiException, UpdateException, PermissionBackendException,
           ConfigInvalidException {
-    if (input.reviewer == null) {
-      throw new BadRequestException("missing reviewer field");
-    }
-
     ReviewerModification modification =
         reviewerModifier.prepare(rsrc.getNotes(), rsrc.getUser(), input, true);
     if (modification.op == null) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
index 1b06b7b..3771bb9 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PostReviewIT.java
@@ -59,6 +59,7 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.api.changes.ReviewerInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.client.Side;
@@ -825,6 +826,41 @@
   }
 
   @Test
+  public void rejectAddingReviewerIfReviewerUserIdentifierIsMissing() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    // Add reviewer with ReviewerInput where the 'reviewer' field is not set.
+    ReviewerInput reviewerInput = new ReviewerInput();
+    reviewerInput.state = ReviewerState.REVIEWER;
+
+    ReviewInput reviewInput = ReviewInput.create();
+    reviewInput.reviewers = ImmutableList.of(reviewerInput);
+
+    ReviewResult reviewResult = gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+    assertThat(reviewResult.error).isEqualTo("error adding reviewer");
+    assertThat(reviewResult.reviewers.keySet()).containsExactly(reviewerInput.reviewer);
+    assertThat(reviewResult.reviewers.get(reviewerInput.reviewer).error)
+        .isEqualTo("reviewer user identifier is required");
+
+    // Add reviewer with ReviewerInput where the 'reviewer' field is set to an empty string.
+    reviewerInput.reviewer = "";
+    reviewResult = gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+    assertThat(reviewResult.error).isEqualTo("error adding reviewer");
+    assertThat(reviewResult.reviewers.keySet()).containsExactly(reviewerInput.reviewer);
+    assertThat(reviewResult.reviewers.get(reviewerInput.reviewer).error)
+        .isEqualTo("reviewer user identifier is required");
+
+    // Add reviewer with ReviewerInput where the 'reviewer' field is set to an empty string after
+    // trimming it.
+    reviewerInput.reviewer = "   ";
+    reviewResult = gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+    assertThat(reviewResult.error).isEqualTo("error adding reviewer");
+    assertThat(reviewResult.reviewers.keySet()).containsExactly(reviewerInput.reviewer);
+    assertThat(reviewResult.reviewers.get(reviewerInput.reviewer).error)
+        .isEqualTo("reviewer user identifier is required");
+  }
+
+  @Test
   public void deletingReviewers() throws Exception {
     PushOneCommit.Result r = createChange();
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index 6cadf33..ca3a345 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -323,11 +323,19 @@
   }
 
   @Test
-  public void rejectMissingEmail() throws Exception {
+  public void rejectIfReviewerUserIdentifierIsMissing() throws Exception {
     PushOneCommit.Result r = createChange();
 
-    ReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer("");
-    assertThat(result.error).isEqualTo(" is not a valid user identifier");
+    ReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer((String) null);
+    assertThat(result.error).isEqualTo("reviewer user identifier is required");
+    assertThat(result.reviewers).isNull();
+
+    result = gApi.changes().id(r.getChangeId()).addReviewer("");
+    assertThat(result.error).isEqualTo("reviewer user identifier is required");
+    assertThat(result.reviewers).isNull();
+
+    result = gApi.changes().id(r.getChangeId()).addReviewer("   ");
+    assertThat(result.error).isEqualTo("reviewer user identifier is required");
     assertThat(result.reviewers).isNull();
   }