Merge "RestApiModule: use existing overload methods"
diff --git a/javatests/com/google/gerrit/acceptance/rest/AbstractRestApiBindingsTest.java b/javatests/com/google/gerrit/acceptance/rest/AbstractRestApiBindingsTest.java
new file mode 100644
index 0000000..26bc345
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/AbstractRestApiBindingsTest.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2018 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;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.apache.http.HttpStatus.SC_FORBIDDEN;
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import java.util.List;
+import java.util.Optional;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Ignore;
+
+/**
+ * Base class for testing the REST API bindings.
+ *
+ * <p>This test sends a request to each REST endpoint and verifies that an implementation is found
+ * (no '404 Not Found' response) and that the request doesn't fail (no '500 Internal Server Error'
+ * response). It doesn't verify that the REST endpoint works correctly. This is okay since the
+ * purpose of the test is only to verify that the REST endpoint implementations are correctly bound.
+ */
+@Ignore
+public abstract class AbstractRestApiBindingsTest extends AbstractDaemonTest {
+  protected void execute(List<RestCall> restCalls, String... args) throws Exception {
+    execute(restCalls, () -> {}, args);
+  }
+
+  protected void execute(List<RestCall> restCalls, BeforeRestCall beforeRestCall, String... args)
+      throws Exception {
+    for (RestCall restCall : restCalls) {
+      beforeRestCall.run();
+      execute(restCall, args);
+    }
+  }
+
+  protected void execute(RestCall restCall, String... args) throws Exception {
+    String method = restCall.httpMethod().name();
+    String uri = restCall.uri(args);
+
+    RestResponse response;
+    switch (restCall.httpMethod()) {
+      case GET:
+        response = adminRestSession.get(uri);
+        break;
+      case PUT:
+        response = adminRestSession.put(uri);
+        break;
+      case POST:
+        response = adminRestSession.post(uri);
+        break;
+      case DELETE:
+        response = adminRestSession.delete(uri);
+        break;
+      default:
+        fail("unsupported method: %s", restCall.httpMethod().name());
+        throw new IllegalStateException();
+    }
+
+    int status = response.getStatusCode();
+    String body = response.hasContent() ? response.getEntityContent() : "";
+
+    String msg = String.format("%s %s returned %d: %s", method, uri, status, body);
+    if (restCall.expectedResponseCode().isPresent()) {
+      assertWithMessage(msg).that(status).isEqualTo(restCall.expectedResponseCode().get());
+    } else {
+      assertWithMessage(msg).that(status).isNotIn(ImmutableList.of(SC_FORBIDDEN, SC_NOT_FOUND));
+      assertWithMessage(msg).that(status).isLessThan(SC_INTERNAL_SERVER_ERROR);
+    }
+  }
+
+  enum Method {
+    GET,
+    PUT,
+    POST,
+    DELETE
+  }
+
+  @AutoValue
+  abstract static class RestCall {
+    static RestCall get(String uriFormat) {
+      return builder(Method.GET, uriFormat).build();
+    }
+
+    static RestCall put(String uriFormat) {
+      return builder(Method.PUT, uriFormat).build();
+    }
+
+    static RestCall post(String uriFormat) {
+      return builder(Method.POST, uriFormat).build();
+    }
+
+    static RestCall delete(String uriFormat) {
+      return builder(Method.DELETE, uriFormat).build();
+    }
+
+    static Builder builder(Method httpMethod, String uriFormat) {
+      return new AutoValue_AbstractRestApiBindingsTest_RestCall.Builder()
+          .httpMethod(httpMethod)
+          .uriFormat(uriFormat);
+    }
+
+    abstract Method httpMethod();
+
+    abstract String uriFormat();
+
+    abstract Optional<Integer> expectedResponseCode();
+
+    String uri(String... args) {
+      String uriFormat = uriFormat();
+      int expectedArgNum = StringUtils.countMatches(uriFormat, "%s");
+      checkState(
+          args.length == expectedArgNum,
+          "uriFormat %s needs %s arguments, got only %s: %s",
+          uriFormat,
+          expectedArgNum,
+          args.length,
+          args);
+      return String.format(uriFormat, (Object[]) args);
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+      abstract Builder httpMethod(Method httpMethod);
+
+      abstract Builder uriFormat(String uriFormat);
+
+      abstract Builder expectedResponseCode(int expectedResponseCode);
+
+      abstract RestCall build();
+    }
+  }
+
+  @FunctionalInterface
+  public interface BeforeRestCall {
+    void run() throws Exception;
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java
new file mode 100644
index 0000000..8de85a9
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2018 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;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.extensions.common.ChangeInput;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the accounts REST API.
+ *
+ * <p>These tests only verify that the account REST endpoints are correctly bound, they do no test
+ * the functionality of the account REST endpoints (for details see JavaDoc on {@link
+ * AbstractRestApiBindingsTest}).
+ */
+public class AccountsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+  /**
+   * Account REST endpoints to be tested, each URL contains a placeholder for the account
+   * identifier.
+   */
+  private static final ImmutableList<RestCall> ACCOUNT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/accounts/%s"),
+          RestCall.put("/accounts/%s"),
+          RestCall.get("/accounts/%s/detail"),
+          RestCall.get("/accounts/%s/name"),
+          RestCall.put("/accounts/%s/name"),
+          RestCall.delete("/accounts/%s/name"),
+          RestCall.get("/accounts/%s/username"),
+          RestCall.put("/accounts/%s/username"),
+          RestCall.get("/accounts/%s/active"),
+          RestCall.put("/accounts/%s/active"),
+          RestCall.delete("/accounts/%s/active"),
+          RestCall.put("/accounts/%s/password.http"),
+          RestCall.delete("/accounts/%s/password.http"),
+          RestCall.get("/accounts/%s/status"),
+          RestCall.put("/accounts/%s/status"),
+          RestCall.get("/accounts/%s/avatar"),
+          RestCall.get("/accounts/%s/avatar.change.url"),
+          RestCall.get("/accounts/%s/emails/"),
+          RestCall.put("/accounts/%s/emails/new-email@foo.com"),
+          RestCall.get("/accounts/%s/sshkeys/"),
+          RestCall.post("/accounts/%s/sshkeys/"),
+          RestCall.get("/accounts/%s/watched.projects"),
+          RestCall.post("/accounts/%s/watched.projects"),
+          RestCall.post("/accounts/%s/watched.projects:delete"),
+          RestCall.get("/accounts/%s/groups"),
+          RestCall.get("/accounts/%s/preferences"),
+          RestCall.put("/accounts/%s/preferences"),
+          RestCall.get("/accounts/%s/preferences.diff"),
+          RestCall.put("/accounts/%s/preferences.diff"),
+          RestCall.get("/accounts/%s/preferences.edit"),
+          RestCall.put("/accounts/%s/preferences.edit"),
+          RestCall.get("/accounts/%s/starred.changes"),
+          RestCall.get("/accounts/%s/stars.changes"),
+          RestCall.post("/accounts/%s/index"),
+          RestCall.get("/accounts/%s/agreements"),
+          RestCall.put("/accounts/%s/agreements"),
+          RestCall.get("/accounts/%s/external.ids"),
+          RestCall.post("/accounts/%s/external.ids:delete"),
+          RestCall.get("/accounts/%s/oauthtoken"),
+          RestCall.get("/accounts/%s/capabilities"),
+          RestCall.get("/accounts/%s/capabilities/viewPlugins"));
+
+  /**
+   * Email REST endpoints to be tested, each URL contains a placeholders for the account and email
+   * identifier.
+   */
+  private static final ImmutableList<RestCall> EMAIL_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/accounts/%s/emails/%s"),
+          RestCall.put("/accounts/%s/emails/%s"),
+          RestCall.put("/accounts/%s/emails/%s/preferred"),
+
+          // email deletion must be tested last
+          RestCall.delete("/accounts/%s/emails/%s"));
+
+  /**
+   * SSH key REST endpoints to be tested, each URL contains a placeholders for the account and SSH
+   * key identifier.
+   */
+  private static final ImmutableList<RestCall> SSH_KEY_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/accounts/%s/sshkeys/%s"),
+
+          // SSH key deletion must be tested last
+          RestCall.delete("/accounts/%s/sshkeys/%s"));
+
+  /**
+   * Star REST endpoints to be tested, each URL contains a placeholders for the account and change
+   * identifier.
+   */
+  private static final ImmutableList<RestCall> STAR_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.put("/accounts/%s/starred.changes/%s"),
+          RestCall.delete("/accounts/%s/starred.changes/%s"),
+          RestCall.get("/accounts/%s/stars.changes/%s"),
+          RestCall.post("/accounts/%s/stars.changes/%s"));
+
+  @Test
+  public void accountEndpoints() throws Exception {
+    execute(ACCOUNT_ENDPOINTS, "self");
+  }
+
+  @Test
+  public void emailEndpoints() throws Exception {
+    execute(EMAIL_ENDPOINTS, "self", admin.email);
+  }
+
+  @Test
+  @UseSsh
+  public void sshKeyEndpoints() throws Exception {
+    String sshKeySeq = Integer.toString(gApi.accounts().self().listSshKeys().size());
+    execute(SSH_KEY_ENDPOINTS, "self", sshKeySeq);
+  }
+
+  @Test
+  public void starEndpoints() throws Exception {
+    ChangeInput ci = new ChangeInput(project.get(), "master", "Test change");
+    String changeId = gApi.changes().create(ci).get().id;
+    execute(STAR_ENDPOINTS, "self", changeId);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/BUILD b/javatests/com/google/gerrit/acceptance/rest/BUILD
new file mode 100644
index 0000000..b4940bc
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/BUILD
@@ -0,0 +1,21 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "rest_bindings",
+    labels = ["rest"],
+    deps = [":util"],
+)
+
+java_library(
+    name = "util",
+    testonly = 1,
+    srcs = [
+        "AbstractRestApiBindingsTest.java",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/acceptance:lib",
+        "//lib/commons:lang",
+    ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/rest/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/ChangesRestApiBindingsIT.java
new file mode 100644
index 0000000..53c544d
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/ChangesRestApiBindingsIT.java
@@ -0,0 +1,449 @@
+// Copyright (C) 2018 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;
+
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.GET;
+import static com.google.gerrit.extensions.common.testing.RobotCommentInfoSubject.assertThatList;
+import static java.util.stream.Collectors.toList;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
+import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.FixReplacementInfo;
+import com.google.gerrit.extensions.common.FixSuggestionInfo;
+import com.google.gerrit.extensions.common.RobotCommentInfo;
+import com.google.gerrit.reviewdb.client.Patch;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the changes REST API.
+ *
+ * <p>These tests only verify that the change REST endpoints are correctly bound, they do no test
+ * the functionality of the change REST endpoints (for details see JavaDoc on {@link
+ * AbstractRestApiBindingsTest}).
+ */
+public class ChangesRestApiBindingsIT extends AbstractRestApiBindingsTest {
+  /**
+   * Change REST endpoints to be tested, each URL contains a placeholder for the change identifier.
+   */
+  private static final ImmutableList<RestCall> CHANGE_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s"),
+          RestCall.get("/changes/%s/detail"),
+          RestCall.get("/changes/%s/topic"),
+          RestCall.put("/changes/%s/topic"),
+          RestCall.delete("/changes/%s/topic"),
+          RestCall.get("/changes/%s/in"),
+          RestCall.get("/changes/%s/hashtags"),
+          RestCall.post("/changes/%s/hashtags"),
+          RestCall.get("/changes/%s/comments"),
+          RestCall.get("/changes/%s/robotcomments"),
+          RestCall.get("/changes/%s/drafts"),
+          RestCall.get("/changes/%s/assignee"),
+          RestCall.get("/changes/%s/past_assignees"),
+          RestCall.put("/changes/%s/assignee"),
+          RestCall.delete("/changes/%s/assignee"),
+          RestCall.post("/changes/%s/private"),
+          RestCall.post("/changes/%s/private.delete"),
+          RestCall.delete("/changes/%s/private"),
+          RestCall.post("/changes/%s/wip"),
+          RestCall.post("/changes/%s/ready"),
+          RestCall.put("/changes/%s/ignore"),
+          RestCall.put("/changes/%s/unignore"),
+          RestCall.put("/changes/%s/reviewed"),
+          RestCall.put("/changes/%s/unreviewed"),
+          RestCall.get("/changes/%s/messages"),
+          RestCall.put("/changes/%s/message"),
+          RestCall.post("/changes/%s/merge"),
+          RestCall.post("/changes/%s/abandon"),
+          RestCall.post("/changes/%s/move"),
+          RestCall.post("/changes/%s/rebase"),
+          RestCall.post("/changes/%s/restore"),
+          RestCall.post("/changes/%s/revert"),
+          RestCall.get("/changes/%s/pure_revert"),
+          RestCall.post("/changes/%s/submit"),
+          RestCall.get("/changes/%s/submitted_together"),
+          RestCall.post("/changes/%s/index"),
+          RestCall.get("/changes/%s/check"),
+          RestCall.post("/changes/%s/check"),
+          RestCall.get("/changes/%s/reviewers"),
+          RestCall.post("/changes/%s/reviewers"),
+          RestCall.get("/changes/%s/suggest_reviewers"),
+          RestCall.builder(GET, "/changes/%s/revisions")
+              // GET /changes/<change-id>/revisions is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+          RestCall.get("/changes/%s/edit"),
+          RestCall.post("/changes/%s/edit:rebase"),
+          RestCall.get("/changes/%s/edit:message"),
+          RestCall.put("/changes/%s/edit:message"),
+
+          // Publish edit and create a new edit
+          RestCall.post("/changes/%s/edit:publish"),
+          RestCall.put("/changes/%s/edit/a.txt"),
+
+          // Deletion of change edit and change must be tested last
+          RestCall.delete("/changes/%s/edit"),
+          RestCall.delete("/changes/%s"));
+
+  /**
+   * Change REST endpoints to be tested with NoteDb, each URL contains a placeholder for the change
+   * identifier.
+   */
+  private static final ImmutableList<RestCall> CHANGE_ENDPOINTS_NOTEDB =
+      ImmutableList.of(RestCall.post("/changes/%s/rebuild.notedb"));
+
+  /**
+   * Reviewer REST endpoints to be tested, each URL contains placeholders for the change identifier
+   * and the reviewer identifier.
+   */
+  private static final ImmutableList<RestCall> REVIEWER_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/reviewers/%s"),
+          RestCall.get("/changes/%s/reviewers/%s/votes"),
+          RestCall.post("/changes/%s/reviewers/%s/delete"),
+          RestCall.delete("/changes/%s/reviewers/%s"));
+
+  /**
+   * Vote REST endpoints to be tested, each URL contains placeholders for the change identifier, the
+   * reviewer identifier and the label identifier.
+   */
+  private static final ImmutableList<RestCall> VOTE_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.post("/changes/%s/reviewers/%s/votes/%s/delete"),
+          RestCall.delete("/changes/%s/reviewers/%s/votes/%s"));
+
+  /**
+   * Revision REST endpoints to be tested, each URL contains placeholders for the change identifier
+   * and the revision identifier.
+   */
+  private static final ImmutableList<RestCall> REVISION_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/revisions/%s/actions"),
+          RestCall.post("/changes/%s/revisions/%s/cherrypick"),
+          RestCall.get("/changes/%s/revisions/%s/commit"),
+          RestCall.get("/changes/%s/revisions/%s/mergeable"),
+          RestCall.get("/changes/%s/revisions/%s/related"),
+          RestCall.get("/changes/%s/revisions/%s/review"),
+          RestCall.post("/changes/%s/revisions/%s/review"),
+          RestCall.get("/changes/%s/revisions/%s/preview_submit"),
+          RestCall.post("/changes/%s/revisions/%s/submit"),
+          RestCall.get("/changes/%s/revisions/%s/submit_type"),
+          RestCall.post("/changes/%s/revisions/%s/test.submit_rule"),
+          RestCall.post("/changes/%s/revisions/%s/test.submit_type"),
+          RestCall.post("/changes/%s/revisions/%s/rebase"),
+          RestCall.get("/changes/%s/revisions/%s/description"),
+          RestCall.put("/changes/%s/revisions/%s/description"),
+          RestCall.get("/changes/%s/revisions/%s/patch"),
+          RestCall.get("/changes/%s/revisions/%s/archive"),
+          RestCall.get("/changes/%s/revisions/%s/mergelist"),
+          RestCall.get("/changes/%s/revisions/%s/reviewers"),
+          RestCall.get("/changes/%s/revisions/%s/drafts"),
+          RestCall.put("/changes/%s/revisions/%s/drafts"),
+          RestCall.get("/changes/%s/revisions/%s/comments"),
+          RestCall.get("/changes/%s/revisions/%s/robotcomments"),
+          RestCall.builder(GET, "/changes/%s/revisions/%s/fixes")
+              // GET /changes/<change>/revisions/<revision>/fixes is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+          RestCall.get("/changes/%s/revisions/%s/files"));
+
+  /**
+   * Revision reviewer REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier and the reviewer identifier.
+   */
+  private static final ImmutableList<RestCall> REVISION_REVIEWER_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/revisions/%s/reviewers/%s"),
+          RestCall.get("/changes/%s/revisions/%s/reviewers/%s/votes"),
+          RestCall.post("/changes/%s/revisions/%s/reviewers/%s/delete"),
+          RestCall.delete("/changes/%s/revisions/%s/reviewers/%s"));
+
+  /**
+   * Revision vote REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier, the reviewer identifier and the label identifier.
+   */
+  private static final ImmutableList<RestCall> REVISION_VOTE_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.post("/changes/%s/revisions/%s/reviewers/%s/votes/%s/delete"),
+          RestCall.delete("/changes/%s/revisions/%s/reviewers/%s/votes/%s"));
+
+  /**
+   * Draft comment REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier and the draft comment identifier.
+   */
+  private static final ImmutableList<RestCall> DRAFT_COMMENT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/revisions/%s/drafts/%s"),
+          RestCall.put("/changes/%s/revisions/%s/drafts/%s"),
+          RestCall.delete("/changes/%s/revisions/%s/drafts/%s"));
+
+  /**
+   * Comment REST endpoints to be tested, each URL contains placeholders for the change identifier,
+   * the revision identifier and the comment identifier.
+   */
+  private static final ImmutableList<RestCall> COMMENT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/changes/%s/revisions/%s/comments/%s"),
+          RestCall.delete("/changes/%s/revisions/%s/comments/%s"),
+          RestCall.post("/changes/%s/revisions/%s/comments/%s/delete"));
+
+  /**
+   * Robot comment REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier and the robot comment identifier.
+   */
+  private static final ImmutableList<RestCall> ROBOT_COMMENT_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/changes/%s/revisions/%s/robotcomments/%s"));
+
+  /**
+   * Fix REST endpoints to be tested, each URL contains placeholders for the change identifier, the
+   * revision identifier and the fix identifier.
+   */
+  private static final ImmutableList<RestCall> FIX_ENDPOINTS =
+      ImmutableList.of(RestCall.post("/changes/%s/revisions/%s/fixes/%s/apply"));
+
+  /**
+   * Revision file REST endpoints to be tested, each URL contains placeholders for the change
+   * identifier, the revision identifier and the file identifier.
+   */
+  private static final ImmutableList<RestCall> REVISION_FILE_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.put("/changes/%s/revisions/%s/files/%s/reviewed"),
+          RestCall.delete("/changes/%s/revisions/%s/files/%s/reviewed"),
+          RestCall.get("/changes/%s/revisions/%s/files/%s/content"),
+          RestCall.get("/changes/%s/revisions/%s/files/%s/download"),
+          RestCall.get("/changes/%s/revisions/%s/files/%s/diff"),
+          RestCall.get("/changes/%s/revisions/%s/files/%s/blame"));
+
+  // TODO(ekempin): Add tests for change message and change edit REST endpoints
+
+  private static final String FILENAME = "test.txt";
+
+  @Test
+  public void changeEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+    gApi.changes().id(changeId).edit().create();
+    execute(CHANGE_ENDPOINTS, changeId);
+  }
+
+  @Test
+  public void changeEndpointsNoteDb() throws Exception {
+    assume().that(notesMigration.readChanges()).isTrue();
+
+    String changeId = createChange().getChangeId();
+    execute(CHANGE_ENDPOINTS_NOTEDB, changeId);
+  }
+
+  @Test
+  public void reviewerEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    AddReviewerInput addReviewerInput = new AddReviewerInput();
+    addReviewerInput.reviewer = user.email;
+
+    execute(
+        REVIEWER_ENDPOINTS,
+        () -> gApi.changes().id(changeId).addReviewer(addReviewerInput),
+        changeId,
+        addReviewerInput.reviewer);
+  }
+
+  @Test
+  public void voteEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    execute(
+        VOTE_ENDPOINTS,
+        () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
+        changeId,
+        admin.email,
+        "Code-Review");
+  }
+
+  @Test
+  public void revisionEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+    execute(REVISION_ENDPOINTS, changeId, "current");
+  }
+
+  @Test
+  public void revisionReviewerEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    AddReviewerInput addReviewerInput = new AddReviewerInput();
+    addReviewerInput.reviewer = user.email;
+
+    execute(
+        REVISION_REVIEWER_ENDPOINTS,
+        () -> gApi.changes().id(changeId).addReviewer(addReviewerInput),
+        changeId,
+        "current",
+        addReviewerInput.reviewer);
+  }
+
+  @Test
+  public void revisionVoteEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    execute(
+        REVISION_VOTE_ENDPOINTS,
+        () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
+        changeId,
+        "current",
+        admin.email,
+        "Code-Review");
+  }
+
+  @Test
+  public void draftCommentEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    for (RestCall restCall : DRAFT_COMMENT_ENDPOINTS) {
+      DraftInput draftInput = new DraftInput();
+      draftInput.path = Patch.COMMIT_MSG;
+      draftInput.side = Side.REVISION;
+      draftInput.line = 1;
+      draftInput.message = "draft comment";
+      CommentInfo draftInfo = gApi.changes().id(changeId).current().createDraft(draftInput).get();
+
+      execute(restCall, changeId, "current", draftInfo.id);
+    }
+  }
+
+  @Test
+  public void commentEndpoints() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    for (RestCall restCall : COMMENT_ENDPOINTS) {
+      DraftInput draftInput = new DraftInput();
+      draftInput.path = Patch.COMMIT_MSG;
+      draftInput.side = Side.REVISION;
+      draftInput.line = 1;
+      draftInput.message = "draft comment";
+      CommentInfo commentInfo = gApi.changes().id(changeId).current().createDraft(draftInput).get();
+
+      ReviewInput reviewInput = new ReviewInput();
+      reviewInput.drafts = DraftHandling.PUBLISH;
+      gApi.changes().id(changeId).current().review(reviewInput);
+
+      execute(restCall, changeId, "current", commentInfo.id);
+    }
+  }
+
+  @Test
+  public void robotCommentEndpoints() throws Exception {
+    assume().that(notesMigration.readChanges()).isTrue();
+
+    String changeId = createChange().getChangeId();
+
+    RobotCommentInput robotCommentInput = new RobotCommentInput();
+    robotCommentInput.robotId = "happyRobot";
+    robotCommentInput.robotRunId = "1";
+    robotCommentInput.line = 1;
+    robotCommentInput.message = "nit: trailing whitespace";
+    robotCommentInput.path = Patch.COMMIT_MSG;
+
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.robotComments =
+        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
+    reviewInput.message = "robot comment test";
+    gApi.changes().id(changeId).current().review(reviewInput);
+
+    List<RobotCommentInfo> robotCommentInfos =
+        gApi.changes().id(changeId).current().robotCommentsAsList();
+    RobotCommentInfo robotCommentInfo = Iterables.getOnlyElement(robotCommentInfos);
+
+    execute(ROBOT_COMMENT_ENDPOINTS, changeId, "current", robotCommentInfo.id);
+  }
+
+  @Test
+  public void fixEndpoints() throws Exception {
+    assume().that(notesMigration.readChanges()).isTrue();
+
+    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
+
+    RobotCommentInput robotCommentInput = new RobotCommentInput();
+    robotCommentInput.robotId = "happyRobot";
+    robotCommentInput.robotRunId = "1";
+    robotCommentInput.line = 1;
+    robotCommentInput.message = "nit: trailing whitespace";
+    robotCommentInput.path = FILENAME;
+
+    FixReplacementInfo fixReplacementInfo = new FixReplacementInfo();
+    fixReplacementInfo.path = FILENAME;
+    fixReplacementInfo.replacement = "some replacement code";
+    fixReplacementInfo.range = createRange(1, 1, 1, 2);
+
+    FixSuggestionInfo fixSuggestionInfo = new FixSuggestionInfo();
+    fixSuggestionInfo.fixId = "An ID which must be overwritten.";
+    fixSuggestionInfo.description = "A description for a suggested fix.";
+    fixSuggestionInfo.replacements = ImmutableList.of(fixReplacementInfo);
+
+    robotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.robotComments =
+        Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
+    reviewInput.message = "robot comment test";
+    gApi.changes().id(changeId).current().review(reviewInput);
+
+    List<RobotCommentInfo> robotCommentInfos =
+        gApi.changes().id(changeId).current().robotCommentsAsList();
+
+    List<String> fixIds = getFixIds(robotCommentInfos);
+    String fixId = Iterables.getOnlyElement(fixIds);
+
+    execute(FIX_ENDPOINTS, changeId, "current", fixId);
+  }
+
+  @Test
+  public void revisionFileEndpoints() throws Exception {
+    String changeId = createChange("Subject", FILENAME, "content").getChangeId();
+    execute(REVISION_FILE_ENDPOINTS, changeId, "current", FILENAME);
+  }
+
+  private static Comment.Range createRange(
+      int startLine, int startCharacter, int endLine, int endCharacter) {
+    Comment.Range range = new Comment.Range();
+    range.startLine = startLine;
+    range.startCharacter = startCharacter;
+    range.endLine = endLine;
+    range.endCharacter = endCharacter;
+    return range;
+  }
+
+  private static List<String> getFixIds(List<RobotCommentInfo> robotComments) {
+    assertThatList(robotComments).isNotNull();
+    return robotComments
+        .stream()
+        .map(robotCommentInfo -> robotCommentInfo.fixSuggestions)
+        .filter(Objects::nonNull)
+        .flatMap(List::stream)
+        .map(fixSuggestionInfo -> fixSuggestionInfo.fixId)
+        .collect(toList());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
new file mode 100644
index 0000000..b930b20
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2018 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;
+
+import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.project.ProjectCacheImpl;
+import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
+import com.google.gson.reflect.TypeToken;
+import java.util.List;
+import java.util.Optional;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the config REST API.
+ *
+ * <p>These tests only verify that the config REST endpoints are correctly bound, they do no test
+ * the functionality of the config REST endpoints (for details see JavaDoc on {@link
+ * AbstractRestApiBindingsTest}).
+ */
+public class ConfigRestApiBindingsIT extends AbstractRestApiBindingsTest {
+  /**
+   * Config REST endpoints to be tested, the URLs contain no placeholders since the only supported
+   * config identifier ('server') can be hard-coded.
+   */
+  private static final ImmutableList<RestCall> CONFIG_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/config/server/version"),
+          RestCall.get("/config/server/info"),
+          RestCall.get("/config/server/preferences"),
+          RestCall.put("/config/server/preferences"),
+          RestCall.get("/config/server/preferences.diff"),
+          RestCall.put("/config/server/preferences.diff"),
+          RestCall.get("/config/server/preferences.edit"),
+          RestCall.put("/config/server/preferences.edit"),
+          RestCall.get("/config/server/top-menus"),
+          RestCall.put("/config/server/email.confirm"),
+          RestCall.post("/config/server/check.consistency"),
+          RestCall.post("/config/server/reload"),
+          RestCall.get("/config/server/summary"),
+          RestCall.get("/config/server/capabilities"),
+          RestCall.get("/config/server/caches"),
+          RestCall.get("/config/server/tasks"));
+
+  /**
+   * Cache REST endpoints to be tested, the URLs contain a placeholder for the cache identifier.
+   * Since there is only supported a single supported config identifier ('server') it can be
+   * hard-coded.
+   */
+  private static final ImmutableList<RestCall> CACHE_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/config/server/caches/%s"));
+
+  /**
+   * Task REST endpoints to be tested, the URLs contain a placeholder for the task identifier. Since
+   * there is only supported a single supported config identifier ('server') it can be hard-coded.
+   */
+  private static final ImmutableList<RestCall> TASK_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/config/server/tasks/%s"),
+
+          // Task deletion must be tested last
+          RestCall.delete("/config/server/tasks/%s"));
+
+  @Test
+  public void configEndpoints() throws Exception {
+    // 'Access Database' is needed for the '/config/server/check.consistency' REST endpoint
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+
+    execute(CONFIG_ENDPOINTS);
+  }
+
+  @Test
+  public void cacheEndpoints() throws Exception {
+    execute(CACHE_ENDPOINTS, ProjectCacheImpl.CACHE_NAME);
+  }
+
+  @Test
+  public void taskEndpoints() throws Exception {
+    RestResponse r = adminRestSession.get("/config/server/tasks/");
+    List<TaskInfo> result =
+        newGson().fromJson(r.getReader(), new TypeToken<List<TaskInfo>>() {}.getType());
+    r.consume();
+
+    Optional<String> id =
+        result
+            .stream()
+            .filter(t -> "Log File Compressor".equals(t.command))
+            .map(t -> t.id)
+            .findFirst();
+    assertThat(id).isPresent();
+
+    execute(TASK_ENDPOINTS, id.get());
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/GroupsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/GroupsRestApiBindingsIT.java
new file mode 100644
index 0000000..4538f75
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/GroupsRestApiBindingsIT.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2018 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;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the groups REST API.
+ *
+ * <p>These tests only verify that the group REST endpoints are correctly bound, they do no test the
+ * functionality of the group REST endpoints (for details see JavaDoc on {@link
+ * AbstractRestApiBindingsTest}).
+ */
+public class GroupsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+  /**
+   * Group REST endpoints to be tested, each URL contains a placeholder for the group identifier.
+   */
+  private static final ImmutableList<RestCall> GROUP_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/groups/%s"),
+          RestCall.put("/groups/%s"),
+          RestCall.get("/groups/%s/detail"),
+          RestCall.get("/groups/%s/name"),
+          RestCall.put("/groups/%s/name"),
+          RestCall.get("/groups/%s/description"),
+          RestCall.put("/groups/%s/description"),
+          RestCall.delete("/groups/%s/description"),
+          RestCall.get("/groups/%s/owner"),
+          RestCall.put("/groups/%s/owner"),
+          RestCall.get("/groups/%s/options"),
+          RestCall.put("/groups/%s/options"),
+          RestCall.post("/groups/%s/members"),
+          RestCall.post("/groups/%s/members.add"),
+          RestCall.post("/groups/%s/members.delete"),
+          RestCall.post("/groups/%s/groups"),
+          RestCall.post("/groups/%s/groups.add"),
+          RestCall.post("/groups/%s/groups.delete"),
+          RestCall.get("/groups/%s/log.audit"),
+          RestCall.post("/groups/%s/index"),
+          RestCall.get("/groups/%s/members"),
+          RestCall.get("/groups/%s/groups"));
+
+  /**
+   * Member REST endpoints to be tested, each URL contains placeholders for the group identifier and
+   * the member identifier.
+   */
+  private static final ImmutableList<RestCall> MEMBER_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/groups/%s/members/%s"),
+          RestCall.put("/groups/%s/members/%s"),
+
+          // Member deletion must be tested last
+          RestCall.delete("/groups/%s/members/%s"));
+
+  /**
+   * Subgroup REST endpoints to be tested, each URL contains placeholders for the group identifier
+   * and the subgroup identifier.
+   */
+  private static final ImmutableList<RestCall> SUBGROUP_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/groups/%s/groups/%s"),
+          RestCall.put("/groups/%s/groups/%s"),
+
+          // Subgroup deletion must be tested last
+          RestCall.delete("/groups/%s/groups/%s"));
+
+  @Test
+  public void groupEndpoints() throws Exception {
+    String group = gApi.groups().create("test-group").get().name;
+    execute(GROUP_ENDPOINTS, group);
+  }
+
+  @Test
+  public void memberEndpoints() throws Exception {
+    String group = gApi.groups().create("test-group").get().name;
+    gApi.groups().id(group).addMembers(admin.email);
+    execute(MEMBER_ENDPOINTS, group, admin.email);
+  }
+
+  @Test
+  public void subgroupEndpoints() throws Exception {
+    String group = gApi.groups().create("test-group").get().name;
+    String subgroup = gApi.groups().create("test-subgroup").get().name;
+    gApi.groups().id(group).addGroups(subgroup);
+    execute(SUBGROUP_ENDPOINTS, group, subgroup);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/PluginsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/PluginsRestApiBindingsIT.java
new file mode 100644
index 0000000..aba677f
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/PluginsRestApiBindingsIT.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2018 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;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
+import com.google.gerrit.extensions.restapi.RawInput;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the plugins REST API.
+ *
+ * <p>These tests only verify that the plugin REST endpoints are correctly bound, they do no test
+ * the functionality of the plugin REST endpoints (for details see JavaDoc on {@link
+ * AbstractRestApiBindingsTest}).
+ */
+public class PluginsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+  /**
+   * Plugin REST endpoints to be tested, each URL contains a placeholder for the plugin identifier.
+   */
+  private static final ImmutableList<RestCall> PLUGIN_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.put("/plugins/%s"),
+
+          // For GET requests prefixing the view name with 'gerrit~' is required.
+          RestCall.get("/plugins/%s/gerrit~status"),
+
+          // POST (and PUT) requests don't require the 'gerrit~' prefix in front of the view name.
+          RestCall.post("/plugins/%s/enable"),
+          RestCall.post("/plugins/%s/disable"),
+          RestCall.post("/plugins/%s/reload"),
+
+          // Plugin deletion must be tested last
+          RestCall.delete("/plugins/%s"));
+
+  private static final String JS_PLUGIN = "Gerrit.install(function(self){});\n";
+  private static final RawInput JS_PLUGIN_CONTENT = RawInputUtil.create(JS_PLUGIN.getBytes(UTF_8));
+
+  @Test
+  @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
+  public void pluginEndpoints() throws Exception {
+    String pluginName = "my-plugin";
+    installPlugin(pluginName);
+    execute(PLUGIN_ENDPOINTS, pluginName);
+  }
+
+  private void installPlugin(String pluginName) throws Exception {
+    InstallPluginInput input = new InstallPluginInput();
+    input.raw = JS_PLUGIN_CONTENT;
+    gApi.plugins().install(pluginName + ".js", input);
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java
new file mode 100644
index 0000000..d59f037
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java
@@ -0,0 +1,246 @@
+// Copyright (C) 2018 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;
+
+import static com.google.gerrit.acceptance.GitUtil.assertPushOk;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.GET;
+import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
+import static com.google.gerrit.server.restapi.project.DashboardsCollection.DEFAULT_DASHBOARD_NAME;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.api.projects.TagInput;
+import com.google.gerrit.reviewdb.client.Project;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the projects REST API.
+ *
+ * <p>These tests only verify that the project REST endpoints are correctly bound, they do no test
+ * the functionality of the project REST endpoints (for details see JavaDoc on {@link
+ * AbstractRestApiBindingsTest}).
+ */
+public class ProjectsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+  private static final ImmutableList<RestCall> PROJECT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s"),
+          RestCall.put("/projects/%s"),
+          RestCall.get("/projects/%s/description"),
+          RestCall.put("/projects/%s/description"),
+          RestCall.delete("/projects/%s/description"),
+          RestCall.get("/projects/%s/parent"),
+          RestCall.put("/projects/%s/parent"),
+          RestCall.get("/projects/%s/config"),
+          RestCall.put("/projects/%s/config"),
+          RestCall.get("/projects/%s/HEAD"),
+          RestCall.put("/projects/%s/HEAD"),
+          RestCall.get("/projects/%s/access"),
+          RestCall.post("/projects/%s/access"),
+          RestCall.put("/projects/%s/access:review"),
+          RestCall.get("/projects/%s/check.access"),
+          RestCall.post("/projects/%s/check.access"),
+          RestCall.put("/projects/%s/ban"),
+          RestCall.get("/projects/%s/statistics.git"),
+          RestCall.post("/projects/%s/index"),
+          RestCall.post("/projects/%s/gc"),
+          RestCall.get("/projects/%s/children"),
+          RestCall.get("/projects/%s/branches"),
+          RestCall.post("/projects/%s/branches:delete"),
+          RestCall.put("/projects/%s/branches/new-branch"),
+          RestCall.get("/projects/%s/tags"),
+          RestCall.post("/projects/%s/tags:delete"),
+          RestCall.put("/projects/%s/tags/new-tag"),
+          RestCall.builder(GET, "/projects/%s/commits")
+              // GET /projects/<project>/branches/<branch>/commits is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+          RestCall.get("/projects/%s/dashboards"));
+
+  /**
+   * Child project REST endpoints to be tested, each URL contains placeholders for the parent
+   * project identifier and the child project identifier.
+   */
+  private static final ImmutableList<RestCall> CHILD_PROJECT_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/projects/%s/children/%s"));
+
+  /**
+   * Branch REST endpoints to be tested, each URL contains placeholders for the project identifier
+   * and the branch identifier.
+   */
+  private static final ImmutableList<RestCall> BRANCH_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s/branches/%s"),
+          RestCall.put("/projects/%s/branches/%s"),
+          RestCall.get("/projects/%s/branches/%s/mergeable"),
+          RestCall.get("/projects/%s/branches/%s/reflog"),
+          RestCall.builder(GET, "/projects/%s/branches/%s/files")
+              // GET /projects/<project>/branches/<branch>/files is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+
+          // Branch deletion must be tested last
+          RestCall.delete("/projects/%s/branches/%s"));
+
+  /**
+   * Branch file REST endpoints to be tested, each URL contains placeholders for the project
+   * identifier, the branch identifier and the file identifier.
+   */
+  private static final ImmutableList<RestCall> BRANCH_FILE_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/projects/%s/branches/%s/files/%s/content"));
+
+  /**
+   * Dashboard REST endpoints to be tested, each URL contains placeholders for the project
+   * identifier and the dashboard identifier.
+   */
+  private static final ImmutableList<RestCall> DASHBOARD_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s/dashboards/%s"),
+          RestCall.put("/projects/%s/dashboards/%s"),
+
+          // Dashboard deletion must be tested last
+          RestCall.delete("/projects/%s/dashboards/%s"));
+
+  /**
+   * Tag REST endpoints to be tested, each URL contains placeholders for the project identifier and
+   * the tag identifier.
+   */
+  private static final ImmutableList<RestCall> TAG_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s/tags/%s"),
+          RestCall.put("/projects/%s/tags/%s"),
+          RestCall.delete("/projects/%s/tags/%s"));
+
+  /**
+   * Commit REST endpoints to be tested, each URL contains placeholders for the project identifier
+   * and the commit identifier.
+   */
+  private static final ImmutableList<RestCall> COMMIT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/projects/%s/commits/%s"),
+          RestCall.get("/projects/%s/commits/%s/in"),
+          RestCall.builder(GET, "/projects/%s/commits/%s/files")
+              // GET /projects/<project>/branches/<branch>/files is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+          RestCall.post("/projects/%s/commits/%s/cherrypick"));
+
+  /**
+   * Commit file REST endpoints to be tested, each URL contains placeholders for the project
+   * identifier, the commit identifier and the file identifier.
+   */
+  private static final ImmutableList<RestCall> COMMIT_FILE_ENDPOINTS =
+      ImmutableList.of(RestCall.get("/projects/%s/commits/%s/files/%s/content"));
+
+  private static final String FILENAME = "test.txt";
+
+  @Test
+  public void projectEndpoints() throws Exception {
+    execute(PROJECT_ENDPOINTS, project.get());
+  }
+
+  @Test
+  public void childProjectEndpoints() throws Exception {
+    Project.NameKey childProject = createProject("test-child-repo", project);
+    execute(CHILD_PROJECT_ENDPOINTS, project.get(), childProject.get());
+  }
+
+  @Test
+  public void branchEndpoints() throws Exception {
+    execute(BRANCH_ENDPOINTS, project.get(), "master");
+  }
+
+  @Test
+  public void branchFileEndpoints() throws Exception {
+    createAndSubmitChange(FILENAME);
+    execute(BRANCH_FILE_ENDPOINTS, project.get(), "master", FILENAME);
+  }
+
+  @Test
+  public void dashboardEndpoints() throws Exception {
+    createDefaultDashboard();
+    execute(DASHBOARD_ENDPOINTS, project.get(), DEFAULT_DASHBOARD_NAME);
+  }
+
+  @Test
+  public void tagEndpoints() throws Exception {
+    String tag = "test-tag";
+    gApi.projects().name(project.get()).tag(tag).create(new TagInput());
+    execute(TAG_ENDPOINTS, project.get(), tag);
+  }
+
+  @Test
+  public void commitEndpoints() throws Exception {
+    String commit = createAndSubmitChange(FILENAME);
+    execute(COMMIT_ENDPOINTS, project.get(), commit);
+  }
+
+  @Test
+  public void commitFileEndpoints() throws Exception {
+    String commit = createAndSubmitChange(FILENAME);
+    execute(COMMIT_FILE_ENDPOINTS, project.get(), commit, FILENAME);
+  }
+
+  private String createAndSubmitChange(String filename) throws Exception {
+    RevCommit c =
+        testRepo
+            .commit()
+            .message("A change")
+            .parent(getRemoteHead())
+            .add(filename, "content")
+            .insertChangeId()
+            .create();
+    String id = GitUtil.getChangeId(testRepo, c).get();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    gApi.changes().id(id).current().review(ReviewInput.approve());
+    gApi.changes().id(id).current().submit();
+    return c.name();
+  }
+
+  private void createDefaultDashboard() throws Exception {
+    String dashboardRef = REFS_DASHBOARDS + "team";
+    grant(project, "refs/meta/*", Permission.CREATE);
+    gApi.projects().name(project.get()).branch(dashboardRef).create(new BranchInput());
+
+    try (Repository r = repoManager.openRepository(project)) {
+      TestRepository<Repository>.CommitBuilder cb =
+          new TestRepository<>(r).branch(dashboardRef).commit();
+      StringBuilder content = new StringBuilder("[dashboard]\n");
+      content.append("title = ").append("Open Changes").append("\n");
+      content.append("[section \"").append("open").append("\"]\n");
+      content.append("query = ").append("is:open").append("\n");
+      cb.add("overview", content.toString());
+      cb.create();
+    }
+
+    try (ProjectConfigUpdate u = updateProject(project)) {
+      u.getConfig().getProject().setLocalDefaultDashboard(dashboardRef + ":overview");
+      u.save();
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/RootCollectionsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/RootCollectionsRestApiBindingsIT.java
new file mode 100644
index 0000000..131ed1a
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/RootCollectionsRestApiBindingsIT.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2018 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;
+
+import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.GET;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+/**
+ * Tests for checking the bindings of the root REST API.
+ *
+ * <p>These tests only verify that the root REST endpoints are correctly bound, they do no test the
+ * functionality of the root REST endpoints (for details see JavaDoc on {@link
+ * AbstractRestApiBindingsTest}).
+ */
+public class RootCollectionsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+  /** Root REST endpoints to be tested, the URLs contain no placeholders. */
+  private static final ImmutableList<RestCall> ROOT_ENDPOINTS =
+      ImmutableList.of(
+          RestCall.get("/access/"),
+          RestCall.get("/accounts/"),
+          RestCall.put("/accounts/new-account"),
+          RestCall.builder(GET, "/config/")
+              // GET /config/ is not implemented
+              .expectedResponseCode(SC_NOT_FOUND)
+              .build(),
+          RestCall.get("/changes/"),
+          RestCall.get("/groups/"),
+          RestCall.put("/groups/new-group"),
+          RestCall.get("/plugins/"),
+          RestCall.put("/plugins/new-plugin"),
+          RestCall.get("/projects/"),
+          RestCall.put("/projects/new-project"));
+
+  @Test
+  public void rootEndpoints() throws Exception {
+    execute(ROOT_ENDPOINTS);
+  }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index 9085971..545d170 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -28,21 +28,32 @@
       :host {
         background-color: var(--dialog-background-color);
         display: block;
-        padding: 1em;
       }
-      header {
+      section {
         display: flex;
+        padding: .5em 1.5em;
       }
-      footer {
+      section:not(:first-of-type) {
+        border-top: 1px solid var(--border-color);
+      }
+      .flexContainer {
         display: flex;
         justify-content: space-between;
         padding-top: .75em;
       }
+      .footer {
+        justify-content: flex-end;
+      }
       .closeButtonContainer {
+        align-items: flex-end;
         display: flex;
         flex: 0;
         justify-content: flex-end;
       }
+      .patchFiles,
+      .archivesContainer {
+        padding-bottom: .5em;
+      }
       .patchFiles {
         margin-right: 2em;
       }
@@ -56,26 +67,26 @@
         margin-right: 0;
       }
       .title {
-        text-align: center;
         flex: 1;
+        font-family: var(--font-family-bold);
+      }
+      .hidden {
+        display: none;
       }
     </style>
-    <header>
+    <section>
       <span class="title">
         Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]]
       </span>
-      <span class="closeButtonContainer">
-        <gr-button id="closeButton"
-            link
-            on-tap="_handleCloseTap">Close</gr-button>
-      </span>
-    </header>
-     <gr-download-commands
+    </section>
+    <section class$="[[_computeShowDownloadCommands(_schemes)]]">
+      <gr-download-commands
           id="downloadCommands"
           commands="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]"
           schemes="[[_schemes]]"
           selected-scheme="{{_selectedScheme}}"></gr-download-commands>
-    <footer>
+    </section>
+    <section class="flexContainer">
       <div class="patchFiles">
         <label>Patch file</label>
         <div>
@@ -104,7 +115,14 @@
           </template>
         </div>
       </div>
-    </footer>
+    </section>
+    <section class="footer">
+      <span class="closeButtonContainer">
+        <gr-button id="closeButton"
+            link
+            on-tap="_handleCloseTap">Close</gr-button>
+      </span>
+    </section>
   </template>
   <script src="gr-download-dialog.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
index fa3e5c9..20449b0 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -172,5 +172,9 @@
         this._selectedScheme = schemes.sort()[0];
       }
     },
+
+    _computeShowDownloadCommands(schemes) {
+      return schemes.length ? '' : 'hidden';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index 84fe8a9..19932c5 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -183,5 +183,10 @@
         MockInteractions.tap(element.$$('.closeButtonContainer gr-button'));
       });
     });
+
+    test('_computeShowDownloadCommands', () => {
+      assert.equal(element._computeShowDownloadCommands([]), 'hidden');
+      assert.equal(element._computeShowDownloadCommands(['test']), '');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index e27a8ea..cde4131 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -528,6 +528,9 @@
       // link, defer to the native behavior.
       if (!path || this.descendedFromClass(e.target, 'pathLink')) { return; }
 
+      // Disregard the event if the click target is in the edit controls.
+      if (this.descendedFromClass(e.target, 'editFileControls')) { return; }
+
       e.preventDefault();
       this._togglePathExpanded(path);
     },
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 5833a9a..29a388e 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -838,6 +838,28 @@
       assert.isTrue(reviewStub.calledOnce);
     });
 
+    test('_handleFileListTap editMode', () => {
+      element._filesByPath = {
+        '/COMMIT_MSG': {},
+        'f1.txt': {},
+        'f2.txt': {},
+      };
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
+      element.editMode = true;
+      flushAsynchronousOperations();
+      const tapSpy = sandbox.spy(element, '_handleFileListTap');
+      const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded');
+
+      // Tap the edit controls. Should be ignored by _handleFileListTap.
+      MockInteractions.tap(element.$$('.editFileControls'));
+      assert.isTrue(tapSpy.calledOnce);
+      assert.isFalse(toggleExpandSpy.called);
+    });
+
     test('patch set from revisions', () => {
       const expected = [
         {num: 4, desc: 'test'},
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
index 7570533..83e99be 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
@@ -15,8 +15,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
+<link rel="import" href="../../../bower_components/paper-tabs/paper-tabs.html">
 <link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -24,17 +27,15 @@
 <dom-module id="gr-download-commands">
   <template>
     <style include="shared-styles">
-      ul {
-        list-style: none;
+      paper-tabs {
+        height: 3rem;
         margin-bottom: .5em;
+        --paper-tabs-selection-bar-color: var(--link-color);
       }
-      li {
-        display: inline-block;
-        margin: 0;
-        padding: 0;
-      }
-      li gr-button {
-        margin-right: 1em;
+      paper-tab {
+        max-width: 15rem;
+        text-transform: uppercase;
+        --paper-tab-ink: var(--link-color);
       }
       label,
       input {
@@ -43,11 +44,6 @@
       label {
         font-family: var(--font-family-bold);
       }
-      li[selected] gr-button {
-        color: var(--primary-text-color);
-        font-family: var(--font-family-bold);
-        text-decoration: none;
-      }
       .schemes {
         display: flex;
         justify-content: space-between;
@@ -55,25 +51,25 @@
       .commands {
         display: flex;
         flex-direction: column;
-        border-bottom: 1px solid var(--border-color);
-        border-top: 1px solid var(--border-color);
-        padding: .5em;
       }
       gr-copy-clipboard {
         width: 60em;
         margin-bottom: .5em;
       }
+      .hidden {
+        display: none;
+      }
     </style>
     <div class="schemes">
-      <ul hidden$="[[!schemes.length]]" hidden>
+      <paper-tabs
+          id="downloadTabs"
+          class$="[[_computeShowTabs(schemes)]]"
+          selected="[[_computeSelected(schemes, selectedScheme)]]"
+          on-selected-changed="_handleTabChange">
         <template is="dom-repeat" items="[[schemes]]" as="scheme">
-          <li selected$="[[_computeSelected(scheme, selectedScheme)]]">
-            <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap">
-              [[scheme]]
-            </gr-button>
-          </li>
+          <paper-tab data-scheme$="[[scheme]]">[[scheme]]</paper-tab>
         </template>
-      </ul>
+      </paper-tabs>
     </div>
     <div class="commands" hidden$="[[!schemes.length]]" hidden>
       <template is="dom-repeat"
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
index 8f513cb..319cd04 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -61,17 +61,24 @@
       });
     },
 
-    _computeSelected(item, selectedItem) {
-      return item === selectedItem;
+    _handleTabChange(e) {
+      const scheme = this.schemes[e.detail.value];
+      if (scheme && scheme !== this.selectedScheme) {
+        this.set('selectedScheme', scheme);
+        if (this._loggedIn) {
+          this.$.restAPI.savePreferences(
+              {download_scheme: this.selectedScheme});
+        }
+      }
     },
 
-    _handleSchemeTap(e) {
-      e.preventDefault();
-      const el = Polymer.dom(e).localTarget;
-      this.selectedScheme = el.getAttribute('data-scheme');
-      if (this._loggedIn) {
-        this.$.restAPI.savePreferences({download_scheme: this.selectedScheme});
-      }
+    _computeSelected(schemes, selectedScheme) {
+      return (schemes.findIndex(scheme => scheme === selectedScheme) || 0)
+          + '';
+    },
+
+    _computeShowTabs(schemes) {
+      return schemes.length > 1 ? '' : 'hidden';
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
index 4fe5569..47219a7 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
@@ -81,30 +81,21 @@
       });
 
       test('element visibility', () => {
-        assert.isFalse(element.$$('ul').hasAttribute('hidden'));
-        assert.isFalse(element.$$('.commands').hasAttribute('hidden'));
+        assert.isFalse(isHidden(element.$$('paper-tabs')));
+        assert.isFalse(isHidden(element.$$('.commands')));
 
         element.schemes = [];
-        assert.isTrue(element.$$('ul').hasAttribute('hidden'));
-        assert.isTrue(element.$$('.commands').hasAttribute('hidden'));
+        assert.isTrue(isHidden(element.$$('paper-tabs')));
+        assert.isTrue(isHidden(element.$$('.commands')));
       });
 
       test('tab selection', () => {
-        flushAsynchronousOperations();
-        let el = element.$$('[data-scheme="http"]').parentElement;
-        assert.isTrue(el.hasAttribute('selected'));
-        for (const scheme of ['repo', 'ssh']) {
-          const el = element.$$('[data-scheme="' + scheme + '"]').parentElement;
-          assert.isFalse(el.hasAttribute('selected'));
-        }
-
+        assert.equal(element.$.downloadTabs.selected, '0');
         MockInteractions.tap(element.$$('[data-scheme="ssh"]'));
-        el = element.$$('[data-scheme="ssh"]').parentElement;
-        assert.isTrue(el.hasAttribute('selected'));
-        for (const scheme of ['http', 'repo']) {
-          const el = element.$$('[data-scheme="' + scheme + '"]').parentElement;
-          assert.isFalse(el.hasAttribute('selected'));
-        }
+        flushAsynchronousOperations();
+
+        assert.equal(element.selectedScheme, 'ssh');
+        assert.equal(element.$.downloadTabs.selected, '2');
       });
 
       test('loads scheme from preferences', done => {
@@ -136,18 +127,18 @@
 
       test('saves scheme to preferences', () => {
         element._loggedIn = true;
-        const savePrefsStub = sinon.stub(element.$.restAPI, 'savePreferences',
+        const savePrefsStub = sandbox.stub(element.$.restAPI, 'savePreferences',
             () => { return Promise.resolve(); });
 
         flushAsynchronousOperations();
 
-        const firstSchemeButton = element.$$('li gr-button[data-scheme]');
+        const repoTab = element.$$('paper-tab[data-scheme="repo"]');
 
-        MockInteractions.tap(firstSchemeButton);
+        MockInteractions.tap(repoTab);
 
         assert.isTrue(savePrefsStub.called);
         assert.equal(savePrefsStub.lastCall.args[0].download_scheme,
-            firstSchemeButton.getAttribute('data-scheme'));
+            repoTab.getAttribute('data-scheme'));
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 5da46ae..dcb428f 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -78,10 +78,6 @@
       },
     },
 
-    listeners: {
-      tap: '_handleTap',
-    },
-
     behaviors: [
       Gerrit.BaseUrlBehavior,
       Gerrit.KeyboardShortcutBehavior,
@@ -294,10 +290,5 @@
     _computeIsDownload(link) {
       return !!link.download;
     },
-
-    _handleTap(e) {
-      e.preventDefault();
-      e.stopImmediatePropagation();
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 250c416..456f235 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -112,21 +112,16 @@
     });
 
     test('non link items', () => {
-      const nativeTapHandler = sandbox.stub();
       const item0 = {name: 'item one', id: 'foo'};
       element.items = [item0, {name: 'item two', id: 'bar'}];
       const fooTapped = sandbox.stub();
       const tapped = sandbox.stub();
       element.addEventListener('tap-item-foo', fooTapped);
       element.addEventListener('tap-item', tapped);
-      element.addEventListener('tap', nativeTapHandler);
       flushAsynchronousOperations();
-
       MockInteractions.tap(element.$$('.itemAction'));
       assert.isTrue(fooTapped.called);
       assert.isTrue(tapped.called);
-      assert.isFalse(nativeTapHandler.called);
-
       assert.deepEqual(tapped.lastCall.args[0].detail, item0);
     });