Do not allow creations of comments with invalid inReplyTo

We used to throw IllegalStateException until I06db2d8f removed
them.

Instead of returning the IllegalStateException, this change added
BadRequestException in all places that can add inReplyTo to comments
or drafts.

Also fixed a test that allowed invalid inReplyTo when updating drafts.

Also added a way to retrieve tags and unresolved from comments to the
test API, since they are used here.

As a follow-up change, the logic in PostReview, CreateDraftComment
and PutDraftComment should actually be the same logic.

Change-Id: I343425abd918a260f3fc71d5c26d18c42297465a
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperationsImpl.java
index 5f046ca..0218731 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerCommentOperationsImpl.java
@@ -56,6 +56,11 @@
   }
 
   static TestHumanComment toTestComment(HumanComment comment) {
-    return TestHumanComment.builder().uuid(comment.key.uuid).parentUuid(comment.parentUuid).build();
+    return TestHumanComment.builder()
+        .uuid(comment.key.uuid)
+        .parentUuid(comment.parentUuid)
+        .tag(comment.tag)
+        .unresolved(comment.unresolved)
+        .build();
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestHumanComment.java b/java/com/google/gerrit/acceptance/testsuite/change/TestHumanComment.java
index 9bb026f..3a7f2ae 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestHumanComment.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestHumanComment.java
@@ -28,6 +28,12 @@
   /** UUID of another comment to which this comment is a reply. */
   public abstract Optional<String> parentUuid();
 
+  /** Tag of a comment. */
+  public abstract Optional<String> tag();
+
+  /** Unresolved state of a comment. */
+  public abstract boolean unresolved();
+
   static Builder builder() {
     return new AutoValue_TestHumanComment.Builder();
   }
@@ -38,6 +44,10 @@
 
     abstract Builder parentUuid(@Nullable String parentUuid);
 
+    abstract Builder tag(@Nullable String tag);
+
+    abstract Builder unresolved(boolean unresolved);
+
     abstract TestHumanComment build();
   }
 }
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 30913f7..b83eae8 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -189,6 +189,12 @@
         .findFirst();
   }
 
+  public Optional<HumanComment> getPublishedHumanComment(ChangeNotes notes, String uuid) {
+    return publishedHumanCommentsByChange(notes).stream()
+        .filter(c -> c.key.uuid.equals(uuid))
+        .findFirst();
+  }
+
   public Optional<HumanComment> getDraft(ChangeNotes notes, IdentifiedUser user, Comment.Key key) {
     return draftByChangeAuthor(notes, user.getAccountId()).stream()
         .filter(c -> key.equals(c.key))
@@ -205,6 +211,10 @@
     return sort(Lists.newArrayList(notes.getRobotComments().values()));
   }
 
+  public Optional<RobotComment> getRobotComment(ChangeNotes notes, String uuid) {
+    return robotCommentsByChange(notes).stream().filter(c -> c.key.uuid.equals(uuid)).findFirst();
+  }
+
   public List<HumanComment> draftByChange(ChangeNotes notes) {
     List<HumanComment> comments = new ArrayList<>();
     for (Ref ref : getDraftRefs(notes.getChangeId())) {
diff --git a/java/com/google/gerrit/server/change/DraftCommentResource.java b/java/com/google/gerrit/server/change/DraftCommentResource.java
index e0648cf..19a495d 100644
--- a/java/com/google/gerrit/server/change/DraftCommentResource.java
+++ b/java/com/google/gerrit/server/change/DraftCommentResource.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.inject.TypeLiteral;
 
 public class DraftCommentResource implements RestResource {
@@ -50,6 +51,10 @@
     return comment;
   }
 
+  public ChangeNotes getNotes() {
+    return rev.getNotes();
+  }
+
   public String getId() {
     return comment.key.uuid;
   }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index b749e6a..399da8f 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -81,6 +81,11 @@
       throw new BadRequestException("line must be >= 0");
     } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
       throw new BadRequestException("range endLine must be on the same line as the comment");
+    } else if (in.inReplyTo != null
+        && !commentsUtil.getPublishedHumanComment(rsrc.getNotes(), in.inReplyTo).isPresent()
+        && !commentsUtil.getRobotComment(rsrc.getNotes(), in.inReplyTo).isPresent()) {
+      throw new BadRequestException(
+          String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
     }
 
     try (BatchUpdate bu =
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 9064dc8..32c1656 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -616,6 +616,7 @@
         ensureCommentNotOnMagicFilesOfAutoMerge(path, comment);
         ensureRangeIsValid(path, comment.range);
         ensureValidPatchsetLevelComment(path, comment);
+        ensureValidInReplyTo(revision.getNotes(), comment.inReplyTo);
       }
     }
   }
@@ -662,6 +663,16 @@
     }
   }
 
+  private void ensureValidInReplyTo(ChangeNotes changeNotes, String inReplyTo)
+      throws BadRequestException {
+    if (inReplyTo != null
+        && !commentsUtil.getPublishedHumanComment(changeNotes, inReplyTo).isPresent()
+        && !commentsUtil.getRobotComment(changeNotes, inReplyTo).isPresent()) {
+      throw new BadRequestException(
+          String.format("Invalid inReplyTo, comment %s not found", inReplyTo));
+    }
+  }
+
   private void checkRobotComments(
       RevisionResource revision, Map<String, List<RobotCommentInput>> in)
       throws BadRequestException, PatchListNotAvailableException {
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index f327f16..17a8ff4 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DraftCommentResource;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -55,6 +56,7 @@
   private final PatchSetUtil psUtil;
   private final Provider<CommentJson> commentJson;
   private final PatchListCache patchListCache;
+  private final ChangeNotes.Factory changeNotesFactory;
 
   @Inject
   PutDraftComment(
@@ -63,13 +65,15 @@
       CommentsUtil commentsUtil,
       PatchSetUtil psUtil,
       Provider<CommentJson> commentJson,
-      PatchListCache patchListCache) {
+      PatchListCache patchListCache,
+      ChangeNotes.Factory changeNotesFactory) {
     this.updateFactory = updateFactory;
     this.delete = delete;
     this.commentsUtil = commentsUtil;
     this.psUtil = psUtil;
     this.commentJson = commentJson;
     this.patchListCache = patchListCache;
+    this.changeNotesFactory = changeNotesFactory;
   }
 
   @Override
@@ -86,8 +90,12 @@
       throw new BadRequestException("patchset-level comments can't have side, range, or line");
     } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
       throw new BadRequestException("range endLine must be on the same line as the comment");
+    } else if (in.inReplyTo != null
+        && !commentsUtil.getPublishedHumanComment(rsrc.getNotes(), in.inReplyTo).isPresent()
+        && !commentsUtil.getRobotComment(rsrc.getNotes(), in.inReplyTo).isPresent()) {
+      throw new BadRequestException(
+          String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
     }
-
     try (BatchUpdate bu =
         updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Op op = new Op(rsrc.getComment().key, in);
diff --git a/java/com/google/gerrit/testing/TestCommentHelper.java b/java/com/google/gerrit/testing/TestCommentHelper.java
index 889bc18..76a5521 100644
--- a/java/com/google/gerrit/testing/TestCommentHelper.java
+++ b/java/com/google/gerrit/testing/TestCommentHelper.java
@@ -17,6 +17,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -148,13 +149,30 @@
     addRobotComment(targetChangeId, robotCommentInput, "robot comment test");
   }
 
+  public void addRobotComment(Change.Id targetChangeId, RobotCommentInput robotCommentInput)
+      throws Exception {
+    addRobotComment(targetChangeId, robotCommentInput, "robot comment test");
+  }
+
   public void addRobotComment(
       String targetChangeId, RobotCommentInput robotCommentInput, String message) throws Exception {
+    ReviewInput reviewInput = createReviewInput(robotCommentInput, message);
+    gApi.changes().id(targetChangeId).current().review(reviewInput);
+  }
+
+  public void addRobotComment(
+      Change.Id targetChangeId, RobotCommentInput robotCommentInput, String message)
+      throws Exception {
+    ReviewInput reviewInput = createReviewInput(robotCommentInput, message);
+    gApi.changes().id(targetChangeId.get()).current().review(reviewInput);
+  }
+
+  private ReviewInput createReviewInput(RobotCommentInput robotCommentInput, String message) {
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.robotComments =
         Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
     reviewInput.message = message;
     reviewInput.tag = ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX;
-    gApi.changes().id(targetChangeId).current().review(reviewInput);
+    return reviewInput;
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 27b866b..1ad952c 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.acceptance.api.revision;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.collect.MoreCollectors.onlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.entities.Patch.COMMIT_MSG;
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
@@ -32,8 +34,11 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Patch;
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
@@ -67,6 +72,7 @@
 
 public class RobotCommentsIT extends AbstractDaemonTest {
   @Inject private TestCommentHelper testCommentHelper;
+  @Inject private ChangeOperations changeOperations;
 
   private static final String PLAIN_TEXT_CONTENT_TYPE = "text/plain";
   private static final String GERRIT_COMMIT_MESSAGE_TYPE = "text/x-gerrit-commit-message";
@@ -320,6 +326,58 @@
   }
 
   @Test
+  public void robotCommentInvalidInReplyTo() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
+    input.inReplyTo = "invalid";
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
+    assertThat(ex.getMessage()).contains("inReplyTo");
+  }
+
+  @Test
+  public void canCreateRobotCommentWithRobotCommentAsParent() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    String parentRobotCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
+
+    ReviewInput.RobotCommentInput robotCommentInput =
+        TestCommentHelper.createRobotCommentInputWithMandatoryFields(COMMIT_MSG);
+    robotCommentInput.message = "comment reply";
+    robotCommentInput.inReplyTo = parentRobotCommentUuid;
+    testCommentHelper.addRobotComment(changeId, robotCommentInput);
+
+    RobotCommentInfo resultComment =
+        Iterables.getOnlyElement(
+            gApi.changes().id(changeId.get()).current().robotCommentsAsList().stream()
+                .filter(c -> c.message.equals("comment reply"))
+                .collect(toImmutableSet()));
+    assertThat(resultComment.inReplyTo).isEqualTo(parentRobotCommentUuid);
+  }
+
+  @Test
+  public void canCreateRobotCommentWithHumanCommentAsParent() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    String changeIdString = changeOperations.change(changeId).get().changeId();
+    String parentCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newComment().create();
+
+    ReviewInput.RobotCommentInput robotCommentInput =
+        TestCommentHelper.createRobotCommentInputWithMandatoryFields(COMMIT_MSG);
+    robotCommentInput.message = "comment reply";
+    robotCommentInput.inReplyTo = parentCommentUuid;
+    testCommentHelper.addRobotComment(changeIdString, robotCommentInput);
+
+    RobotCommentInfo resultComment =
+        Iterables.getOnlyElement(
+            gApi.changes().id(changeIdString).current().robotCommentsAsList().stream()
+                .filter(c -> c.message.equals("comment reply"))
+                .collect(toImmutableSet()));
+    assertThat(resultComment.inReplyTo).isEqualTo(parentCommentUuid);
+  }
+
+  @Test
   public void hugeRobotCommentIsRejected() {
     int defaultSizeLimit = 1 << 20;
     fixReplacementInfo.replacement = getStringFor(defaultSizeLimit + 1);
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 88705e1..0cdff4e 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.acceptance.server.change;
 
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.entities.Patch.COMMIT_MSG;
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static com.google.gerrit.truth.MapSubject.assertThatMap;
@@ -51,7 +53,6 @@
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.extensions.common.RobotCommentInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -658,6 +659,34 @@
   }
 
   @Test
+  public void putDraft_humanInReplyTo() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    String parentCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newComment().create();
+
+    DraftInput draft = newDraft(COMMIT_MSG, Side.REVISION, 0, "foo");
+    draft.inReplyTo = parentCommentUuid;
+    String createdDraftUuid = addDraft(changeId, draft).id;
+    TestHumanComment actual =
+        changeOperations.change(changeId).draftComment(createdDraftUuid).get();
+    assertThat(actual.parentUuid()).hasValue(parentCommentUuid);
+  }
+
+  @Test
+  public void putDraft_robotInReplyTo() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    String parentRobotCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
+
+    DraftInput draft = newDraft(COMMIT_MSG, Side.REVISION, 0, "foo");
+    draft.inReplyTo = parentRobotCommentUuid;
+    String createdDraftUuid = addDraft(changeId, draft).id;
+    TestHumanComment actual =
+        changeOperations.change(changeId).draftComment(createdDraftUuid).get();
+    assertThat(actual.parentUuid()).hasValue(parentRobotCommentUuid);
+  }
+
+  @Test
   public void putDraft_idMismatch() throws Exception {
     String file = "file";
     PushOneCommit.Result r = createChange();
@@ -702,6 +731,16 @@
   }
 
   @Test
+  public void putDraft_invalidInReplyTo() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    DraftInput draft = newDraft(COMMIT_MSG, Side.REVISION, 0, "foo");
+    draft.inReplyTo = "invalid";
+    BadRequestException exception =
+        assertThrows(BadRequestException.class, () -> addDraft(changeId, draft));
+    assertThat(exception.getMessage()).contains(String.format("%s not found", draft.inReplyTo));
+  }
+
+  @Test
   public void putDraft_updatePath() throws Exception {
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
@@ -715,22 +754,62 @@
   }
 
   @Test
-  public void putDraft_updateInReplyToAndTag() throws Exception {
-    PushOneCommit.Result r = createChange();
-    String changeId = r.getChangeId();
-    String revId = r.getCommit().getName();
-    DraftInput draftInput1 = newDraft(FILE_NAME, Side.REVISION, 0, "foo");
-    CommentInfo commentInfo = addDraft(changeId, revId, draftInput1);
-    DraftInput draftInput2 = newDraft(FILE_NAME, Side.REVISION, 0, "bar");
-    String inReplyTo = "in_reply_to";
+  public void putDraft_updateInvalidInReplyTo() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    DraftInput originalDraftInput = newDraft(FILE_NAME, Side.REVISION, 0, "foo");
+    CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
+
+    DraftInput updatedDraftInput = newDraft(FILE_NAME, Side.REVISION, 0, "bar");
+    updatedDraftInput.inReplyTo = "invalid";
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () -> updateDraft(changeId, updatedDraftInput, originalDraft.id));
+    assertThat(exception.getMessage()).contains(String.format("Invalid inReplyTo"));
+  }
+
+  @Test
+  public void putDraft_updateHumanInReplyTo() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    String parentCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newComment().create();
+    DraftInput originalDraftInput = newDraft(FILE_NAME, Side.REVISION, 0, "foo");
+    CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
+
+    DraftInput updateDraftInput = newDraft(FILE_NAME, Side.REVISION, 0, "bar");
+    updateDraftInput.inReplyTo = parentCommentUuid;
+    updateDraft(changeId, updateDraftInput, originalDraft.id);
+    assertThat(changeOperations.change(changeId).draftComment(originalDraft.id).get().parentUuid())
+        .hasValue(parentCommentUuid);
+  }
+
+  @Test
+  public void putDraft_updateRobotInReplyTo() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    String parentRobotCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
+    DraftInput originalDraftInput = newDraft(FILE_NAME, Side.REVISION, 0, "foo");
+    CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
+
+    DraftInput updateDraftInput = newDraft(FILE_NAME, Side.REVISION, 0, "bar");
+    updateDraftInput.inReplyTo = parentRobotCommentUuid;
+    updateDraft(changeId, updateDraftInput, originalDraft.id);
+    assertThat(changeOperations.change(changeId).draftComment(originalDraft.id).get().parentUuid())
+        .hasValue(parentRobotCommentUuid);
+  }
+
+  @Test
+  public void putDraft_updateTag() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    DraftInput originalDraftInput = newDraft(FILE_NAME, Side.REVISION, 0, "foo");
+    CommentInfo originalDraft = addDraft(changeId, originalDraftInput);
+
+    DraftInput updateDraftInput = newDraft(FILE_NAME, Side.REVISION, 0, "bar");
     String tag = "täg";
-    draftInput2.inReplyTo = inReplyTo;
-    draftInput2.tag = tag;
-    updateDraft(changeId, revId, draftInput2, commentInfo.id);
-    com.google.gerrit.entities.Comment comment =
-        Iterables.getOnlyElement(commentsUtil.draftByChange(r.getChange().notes()));
-    assertThat(comment.parentUuid).isEqualTo(inReplyTo);
-    assertThat(comment.tag).isEqualTo(tag);
+    updateDraftInput.tag = tag;
+    updateDraft(changeId, updateDraftInput, originalDraft.id);
+    assertThat(changeOperations.change(changeId).draftComment(originalDraft.id).get().tag())
+        .hasValue(tag);
   }
 
   @Test
@@ -1474,26 +1553,73 @@
 
   @Test
   public void canCreateHumanCommentWithRobotCommentAsParentAndUnsetUnresolved() throws Exception {
-    PushOneCommit.Result result = createChange();
-    String changeId = result.getChangeId();
-    String ps1 = result.getCommit().name();
+    Change.Id changeId = changeOperations.newChange().create();
+    String parentRobotCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
 
-    testCommentHelper.addRobotComment(
-        result.getChangeId(),
-        TestCommentHelper.createRobotCommentInputWithMandatoryFields(FILE_NAME));
-    RobotCommentInfo robotCommentInfo =
-        Iterables.getOnlyElement(gApi.changes().id(changeId).current().robotCommentsAsList());
+    CommentInput createdCommentInput = newComment(COMMIT_MSG, "comment reply");
+    createdCommentInput.inReplyTo = parentRobotCommentUuid;
+    createdCommentInput.unresolved = null;
+    addComments(changeId, createdCommentInput);
 
-    CommentInput comment = newComment(FILE_NAME, "comment 1 reply");
-    comment.inReplyTo = robotCommentInfo.id;
-    comment.unresolved = null;
-    addComments(changeId, ps1, comment);
+    CommentInfo resultNewComment =
+        Iterables.getOnlyElement(
+            getPublishedCommentsAsList(changeId).stream()
+                .filter(c -> c.message.equals("comment reply"))
+                .collect(toImmutableSet()));
 
-    CommentInfo resultComment = Iterables.getOnlyElement(getPublishedCommentsAsList(changeId));
-    assertThat(resultComment.inReplyTo).isEqualTo(robotCommentInfo.id);
+    assertThat(resultNewComment.inReplyTo).isEqualTo(parentRobotCommentUuid);
 
     // Default unresolved is false.
-    assertThat(resultComment.unresolved).isFalse();
+    assertThat(resultNewComment.unresolved).isFalse();
+  }
+
+  @Test
+  public void canCreateHumanCommentWithHumanCommentAsParent() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    String parentCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newComment().create();
+
+    CommentInput createdCommentInput = newComment(COMMIT_MSG, "comment reply");
+    createdCommentInput.inReplyTo = parentCommentUuid;
+    addComments(changeId, createdCommentInput);
+
+    CommentInfo resultNewComment =
+        Iterables.getOnlyElement(
+            getPublishedCommentsAsList(changeId).stream()
+                .filter(c -> c.message.equals("comment reply"))
+                .collect(toImmutableSet()));
+    assertThat(resultNewComment.inReplyTo).isEqualTo(parentCommentUuid);
+  }
+
+  @Test
+  public void canCreateHumanCommentWithRobotCommentAsParent() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+    String parentRobotCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newRobotComment().create();
+
+    CommentInput createdCommentInput = newComment(COMMIT_MSG, "comment reply");
+    createdCommentInput.inReplyTo = parentRobotCommentUuid;
+    addComments(changeId, createdCommentInput);
+
+    CommentInfo resultNewComment =
+        Iterables.getOnlyElement(
+            getPublishedCommentsAsList(changeId).stream()
+                .filter(c -> c.message.equals("comment reply"))
+                .collect(toImmutableSet()));
+    assertThat(resultNewComment.inReplyTo).isEqualTo(parentRobotCommentUuid);
+  }
+
+  @Test
+  public void cannotCreateCommentWithInvalidInReplyTo() throws Exception {
+    Change.Id changeId = changeOperations.newChange().create();
+
+    CommentInput comment = newComment(COMMIT_MSG, "comment 1 reply");
+    comment.inReplyTo = "invalid";
+
+    BadRequestException exception =
+        assertThrows(BadRequestException.class, () -> addComments(changeId, comment));
+    assertThat(exception.getMessage()).contains(String.format("%s not found", comment.inReplyTo));
   }
 
   private List<CommentInfo> getRevisionComments(String changeId, String revId) throws Exception {
@@ -1510,6 +1636,12 @@
     return comment;
   }
 
+  private void addComments(Change.Id changeId, CommentInput... commentInputs) throws Exception {
+    ReviewInput input = new ReviewInput();
+    input.comments = Arrays.stream(commentInputs).collect(groupingBy(c -> c.path));
+    gApi.changes().id(changeId.get()).current().review(input);
+  }
+
   private void addComments(String changeId, String revision, CommentInput... commentInputs)
       throws Exception {
     ReviewInput input = new ReviewInput();
@@ -1517,6 +1649,12 @@
     gApi.changes().id(changeId).revision(revision).review(input);
   }
 
+  private void addComments(String changeId, CommentInput... commentInputs) throws Exception {
+    ReviewInput input = new ReviewInput();
+    input.comments = Arrays.stream(commentInputs).collect(groupingBy(c -> c.path));
+    gApi.changes().id(changeId).current().review(input);
+  }
+
   /**
    * All the commits, which contain the target comment before, should still contain the comment with
    * the updated message. All the other metas of the commits should be exactly the same.
@@ -1610,11 +1748,31 @@
     return gApi.changes().id(changeId).revision(revId).createDraft(in).get();
   }
 
+  private CommentInfo addDraft(Change.Id changeId, String revId, DraftInput in) throws Exception {
+    return gApi.changes().id(changeId.get()).revision(revId).createDraft(in).get();
+  }
+
+  private CommentInfo addDraft(String changeId, DraftInput in) throws Exception {
+    return gApi.changes().id(changeId).current().createDraft(in).get();
+  }
+
+  private CommentInfo addDraft(Change.Id changeId, DraftInput in) throws Exception {
+    return gApi.changes().id(changeId.get()).current().createDraft(in).get();
+  }
+
   private void updateDraft(String changeId, String revId, DraftInput in, String uuid)
       throws Exception {
     gApi.changes().id(changeId).revision(revId).draft(uuid).update(in);
   }
 
+  private void updateDraft(String changeId, DraftInput in, String uuid) throws Exception {
+    gApi.changes().id(changeId).current().draft(uuid).update(in);
+  }
+
+  private void updateDraft(Change.Id changeId, DraftInput in, String uuid) throws Exception {
+    gApi.changes().id(changeId.get()).current().draft(uuid).update(in);
+  }
+
   private void deleteDraft(String changeId, String revId, String uuid) throws Exception {
     gApi.changes().id(changeId).revision(revId).draft(uuid).delete();
   }
@@ -1633,6 +1791,10 @@
     return gApi.changes().id(changeId).commentsAsList();
   }
 
+  private List<CommentInfo> getPublishedCommentsAsList(Change.Id changeId) throws Exception {
+    return gApi.changes().id(changeId.get()).commentsAsList();
+  }
+
   private Map<String, List<CommentInfo>> getDraftComments(String changeId, String revId)
       throws Exception {
     return gApi.changes().id(changeId).revision(revId).drafts();
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
index a6e31eb..0bd6554 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
@@ -1175,6 +1175,39 @@
   }
 
   @Test
+  public void tagOfPublishedCommentCanBeRetrieved() {
+    Change.Id changeId = changeOperations.newChange().create();
+    String childCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newComment().tag("tag").create();
+
+    TestHumanComment comment = changeOperations.change(changeId).comment(childCommentUuid).get();
+
+    assertThat(comment.tag()).value().isEqualTo("tag");
+  }
+
+  @Test
+  public void unresolvedOfUnresolvedPublishedCommentCanBeRetrieved() {
+    Change.Id changeId = changeOperations.newChange().create();
+    String childCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newComment().unresolved().create();
+
+    TestHumanComment comment = changeOperations.change(changeId).comment(childCommentUuid).get();
+
+    assertThat(comment.unresolved()).isTrue();
+  }
+
+  @Test
+  public void unresolvedOfResolvedPublishedCommentCanBeRetrieved() {
+    Change.Id changeId = changeOperations.newChange().create();
+    String childCommentUuid =
+        changeOperations.change(changeId).currentPatchset().newComment().resolved().create();
+
+    TestHumanComment comment = changeOperations.change(changeId).comment(childCommentUuid).get();
+
+    assertThat(comment.unresolved()).isFalse();
+  }
+
+  @Test
   public void draftCommentCanBeRetrieved() {
     Change.Id changeId = changeOperations.newChange().create();
     String commentUuid = changeOperations.change(changeId).currentPatchset().newComment().create();