Merge "Merge branch 'stable-2.16'"
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 2633569..9e2efd8 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -108,19 +108,6 @@
     return project;
   }
 
-  protected TestRepository<?> createProjectWithPush(String name) throws Exception {
-    return createProjectWithPush(name, null, true, getSubmitType());
-  }
-
-  protected TestRepository<?> createProjectWithPush(
-      String name,
-      @Nullable Project.NameKey parent,
-      boolean createEmptyCommit,
-      SubmitType submitType)
-      throws Exception {
-    return cloneProject(createProjectForPush(name, parent, createEmptyCommit, submitType));
-  }
-
   private static AtomicInteger contentCounter = new AtomicInteger(0);
   protected TestRepository<?> superRepo;
   protected Project.NameKey superKey;
diff --git a/javatests/com/google/gerrit/acceptance/git/BUILD b/javatests/com/google/gerrit/acceptance/git/BUILD
index 3541fec..6fa510a 100644
--- a/javatests/com/google/gerrit/acceptance/git/BUILD
+++ b/javatests/com/google/gerrit/acceptance/git/BUILD
@@ -7,6 +7,7 @@
     deps = [
         ":push_for_review",
         ":submodule_util",
+        "//lib/commons:lang",
     ],
 )
 
diff --git a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 96b545c..d3d1ab3 100644
--- a/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -21,15 +21,20 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.change.TestSubmitInput;
 import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
 import java.util.ArrayDeque;
 import java.util.Map;
+import org.apache.commons.lang.RandomStringUtils;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -66,6 +71,8 @@
     return submitByRebaseIfNecessaryConfig();
   }
 
+  @Inject ProjectOperations projectOperations;
+
   @Test
   public void subscriptionUpdateOfManyChanges() throws Exception {
     allowMatchingSubmoduleSubscription(subKey, "refs/heads/master", superKey, "refs/heads/master");
@@ -270,66 +277,60 @@
 
   @Test
   public void updateManySubmodules() throws Exception {
-    Project.NameKey subKey1 = createProjectForPush("sub1", null, true, getSubmitType());
-    Project.NameKey subKey2 = createProjectForPush("sub2", null, true, getSubmitType());
-    Project.NameKey subKey3 = createProjectForPush("sub3", null, true, getSubmitType());
+    Project.NameKey subKey[] = new NameKey[3];
+    TestRepository<?> sub[] = new TestRepository[3];
+    String prefix = RandomStringUtils.randomAlphabetic(8);
+    for (int i = 0; i < subKey.length; i++) {
+      subKey[i] =
+          projectOperations
+              .newProject()
+              .name(prefix + "sub" + i)
+              .withEmptyCommit()
+              .submitType(getSubmitType())
+              .create();
+      grant(subKey[i], "refs/heads/*", Permission.PUSH);
+      grant(subKey[i], "refs/for/refs/heads/*", Permission.SUBMIT);
+      sub[i] = cloneProject(subKey[i]);
+    }
 
-    TestRepository<?> sub1 = cloneProject(subKey1);
-    TestRepository<?> sub2 = cloneProject(subKey2);
-    TestRepository<?> sub3 = cloneProject(subKey3);
-
-    allowMatchingSubmoduleSubscription(subKey1, "refs/heads/master", superKey, "refs/heads/master");
-    allowMatchingSubmoduleSubscription(subKey2, "refs/heads/master", superKey, "refs/heads/master");
-    allowMatchingSubmoduleSubscription(subKey3, "refs/heads/master", superKey, "refs/heads/master");
+    for (int i = 0; i < subKey.length; i++) {
+      allowMatchingSubmoduleSubscription(
+          subKey[i], "refs/heads/master", superKey, "refs/heads/master");
+    }
 
     Config config = new Config();
-    prepareSubmoduleConfigEntry(config, subKey1, "master");
-    prepareSubmoduleConfigEntry(config, subKey2, "master");
-    prepareSubmoduleConfigEntry(config, subKey3, "master");
+    for (int i = 0; i < subKey.length; i++) {
+      prepareSubmoduleConfigEntry(config, subKey[i], "master");
+    }
     pushSubmoduleConfig(superRepo, "master", config);
 
     ObjectId superPreviousId = pushChangeTo(superRepo, "master");
 
-    ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", "same-topic");
-    ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master", "some message", "same-topic");
-    ObjectId sub3Id = pushChangeTo(sub3, "refs/for/master", "some message", "same-topic");
+    ObjectId subId[] = new ObjectId[3];
 
-    approve(getChangeId(sub1, sub1Id).get());
-    approve(getChangeId(sub2, sub2Id).get());
-    approve(getChangeId(sub3, sub3Id).get());
+    for (int i = 0; i < sub.length; i++) {
+      subId[i] = pushChangeTo(sub[i], "refs/for/master", "some message", "same-topic");
+      approve(getChangeId(sub[i], subId[i]).get());
+    }
 
-    gApi.changes().id(getChangeId(sub1, sub1Id).get()).current().submit();
+    gApi.changes().id(getChangeId(sub[0], subId[0]).get()).current().submit();
 
-    expectToHaveSubmoduleState(superRepo, "master", subKey1, sub1, "master");
-    expectToHaveSubmoduleState(superRepo, "master", subKey2, sub2, "master");
-    expectToHaveSubmoduleState(superRepo, "master", subKey3, sub3, "master");
+    for (int i = 0; i < sub.length; i++) {
+      expectToHaveSubmoduleState(superRepo, "master", subKey[i], sub[i], "master");
+    }
 
-    String sub1HEAD =
-        sub1.git()
-            .fetch()
-            .setRemote("origin")
-            .call()
-            .getAdvertisedRef("refs/heads/master")
-            .getObjectId()
-            .name();
-
-    String sub2HEAD =
-        sub2.git()
-            .fetch()
-            .setRemote("origin")
-            .call()
-            .getAdvertisedRef("refs/heads/master")
-            .getObjectId()
-            .name();
-
-    String sub3HEAD =
-        sub3.git()
-            .fetch()
-            .setRemote("origin")
-            .call()
-            .getAdvertisedRef("refs/heads/master")
-            .getObjectId()
-            .name();
+    String heads[] = new String[3];
+    for (int i = 0; i < heads.length; i++) {
+      heads[i] =
+          sub[i]
+              .git()
+              .fetch()
+              .setRemote("origin")
+              .call()
+              .getAdvertisedRef("refs/heads/master")
+              .getObjectId()
+              .name();
+    }
 
     if (getSubmitType() == SubmitType.MERGE_IF_NECESSARY) {
       expectToHaveCommitMessage(
@@ -337,17 +338,17 @@
           "master",
           "Update git submodules\n\n"
               + "* Update "
-              + subKey1.get()
+              + subKey[0].get()
               + " from branch 'master'\n  to "
-              + sub1HEAD
+              + heads[0]
               + "\n\n* Update "
-              + subKey2.get()
+              + subKey[1].get()
               + " from branch 'master'\n  to "
-              + sub2HEAD
+              + heads[1]
               + "\n\n* Update "
-              + subKey3.get()
+              + subKey[2].get()
               + " from branch 'master'\n  to "
-              + sub3HEAD);
+              + heads[2]);
     }
 
     superRepo
diff --git a/javatests/com/google/gerrit/acceptance/rest/AbstractRestApiBindingsTest.java b/javatests/com/google/gerrit/acceptance/rest/AbstractRestApiBindingsTest.java
deleted file mode 100644
index 2bb3dca..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/AbstractRestApiBindingsTest.java
+++ /dev/null
@@ -1,165 +0,0 @@
-// 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_METHOD_NOT_ALLOWED;
-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());
-      if (restCall.expectedMessage().isPresent()) {
-        assertWithMessage(msg).that(body).contains(restCall.expectedMessage().get());
-      }
-    } else {
-      assertWithMessage(msg)
-          .that(status)
-          .isNotIn(ImmutableList.of(SC_FORBIDDEN, SC_NOT_FOUND, SC_METHOD_NOT_ALLOWED));
-      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();
-
-    abstract Optional<String> expectedMessage();
-
-    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 Builder expectedMessage(String expectedMessage);
-
-      abstract RestCall build();
-    }
-  }
-
-  @FunctionalInterface
-  public interface BeforeRestCall {
-    void run() throws Exception;
-  }
-}
diff --git a/javatests/com/google/gerrit/acceptance/rest/BUILD b/javatests/com/google/gerrit/acceptance/rest/BUILD
index b94a98d..84887da 100644
--- a/javatests/com/google/gerrit/acceptance/rest/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/BUILD
@@ -2,23 +2,9 @@
 
 acceptance_tests(
     srcs = glob(["*IT.java"]),
-    group = "rest_bindings",
+    group = "rest_bindings_collection",
     labels = ["rest"],
     deps = [
-        ":util",
         "//java/com/google/gerrit/server/logging",
     ],
 )
-
-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/DeleteOnCollectionIT.java b/javatests/com/google/gerrit/acceptance/rest/DeleteOnCollectionIT.java
index 4de436a..d891d6e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/DeleteOnCollectionIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/DeleteOnCollectionIT.java
@@ -36,13 +36,8 @@
       public void configure() {
         deleteOnCollection(BRANCH_KIND)
             .toInstance(
-                new RestCollectionModifyView<ProjectResource, BranchResource, Object>() {
-                  @Override
-                  public Object apply(ProjectResource parentResource, Object input)
-                      throws Exception {
-                    return Response.none();
-                  }
-                });
+                (RestCollectionModifyView<ProjectResource, BranchResource, Object>)
+                    (parentResource, input) -> Response.none());
       }
     };
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
similarity index 90%
rename from javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java
rename to javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
index b0adba7..7f55744 100644
--- a/javatests/com/google/gerrit/acceptance/rest/AccountsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/AccountsRestApiBindingsIT.java
@@ -12,15 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest;
+package com.google.gerrit.acceptance.rest.binding;
 
-import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.PUT;
+import static com.google.gerrit.acceptance.rest.util.RestApiCallHelper.execute;
+import static com.google.gerrit.acceptance.rest.util.RestCall.Method.PUT;
 import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration;
 import static org.apache.http.HttpStatus.SC_METHOD_NOT_ALLOWED;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.rest.util.RestCall;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.gpg.testing.TestKey;
 import com.google.gerrit.server.ServerInitiated;
@@ -34,10 +37,9 @@
  * 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}).
+ * the functionality of the account REST endpoints.
  */
-public class AccountsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+public class AccountsRestApiBindingsIT extends AbstractDaemonTest {
   @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
 
   /**
@@ -143,12 +145,12 @@
 
   @Test
   public void accountEndpoints() throws Exception {
-    execute(ACCOUNT_ENDPOINTS, "self");
+    execute(adminRestSession, ACCOUNT_ENDPOINTS, "self");
   }
 
   @Test
   public void emailEndpoints() throws Exception {
-    execute(EMAIL_ENDPOINTS, "self", admin.email);
+    execute(adminRestSession, EMAIL_ENDPOINTS, "self", admin.email);
   }
 
   @Test
@@ -172,20 +174,20 @@
         .self()
         .putGpgKeys(ImmutableList.of(key.getPublicKeyArmored()), ImmutableList.of());
 
-    execute(GPG_KEY_ENDPOINTS, "self", id);
+    execute(adminRestSession, GPG_KEY_ENDPOINTS, "self", id);
   }
 
   @Test
   @UseSsh
   public void sshKeyEndpoints() throws Exception {
     String sshKeySeq = Integer.toString(gApi.accounts().self().listSshKeys().size());
-    execute(SSH_KEY_ENDPOINTS, "self", sshKeySeq);
+    execute(adminRestSession, 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);
+    execute(adminRestSession, STAR_ENDPOINTS, "self", changeId);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/BUILD b/javatests/com/google/gerrit/acceptance/rest/binding/BUILD
new file mode 100644
index 0000000..e4242a9
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/BUILD
@@ -0,0 +1,11 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+    srcs = glob(["*IT.java"]),
+    group = "rest_bindings",
+    labels = ["rest"],
+    deps = [
+        "//java/com/google/gerrit/server/logging",
+        "//javatests/com/google/gerrit/acceptance/rest/util",
+    ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/rest/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
similarity index 91%
rename from javatests/com/google/gerrit/acceptance/rest/ChangesRestApiBindingsIT.java
rename to javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 59c0903..628b63f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -12,16 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest;
+package com.google.gerrit.acceptance.rest.binding;
 
 import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.GET;
+import static com.google.gerrit.acceptance.rest.util.RestCall.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.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -43,10 +46,9 @@
  * 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}).
+ * the functionality of the change REST endpoints.
  */
-public class ChangesRestApiBindingsIT extends AbstractRestApiBindingsTest {
+public class ChangesRestApiBindingsIT extends AbstractDaemonTest {
   /**
    * Change REST endpoints to be tested, each URL contains a placeholder for the change identifier.
    */
@@ -279,7 +281,7 @@
   public void changeEndpoints() throws Exception {
     String changeId = createChange().getChangeId();
     gApi.changes().id(changeId).edit().create();
-    execute(CHANGE_ENDPOINTS, changeId);
+    RestApiCallHelper.execute(adminRestSession, CHANGE_ENDPOINTS, changeId);
   }
 
   @Test
@@ -287,7 +289,7 @@
     assume().that(notesMigration.readChanges()).isTrue();
 
     String changeId = createChange().getChangeId();
-    execute(CHANGE_ENDPOINTS_NOTEDB, changeId);
+    RestApiCallHelper.execute(adminRestSession, CHANGE_ENDPOINTS_NOTEDB, changeId);
   }
 
   @Test
@@ -297,7 +299,8 @@
     AddReviewerInput addReviewerInput = new AddReviewerInput();
     addReviewerInput.reviewer = user.email;
 
-    execute(
+    RestApiCallHelper.execute(
+        adminRestSession,
         REVIEWER_ENDPOINTS,
         () -> gApi.changes().id(changeId).addReviewer(addReviewerInput),
         changeId,
@@ -308,7 +311,8 @@
   public void voteEndpoints() throws Exception {
     String changeId = createChange().getChangeId();
 
-    execute(
+    RestApiCallHelper.execute(
+        adminRestSession,
         VOTE_ENDPOINTS,
         () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
         changeId,
@@ -319,7 +323,7 @@
   @Test
   public void revisionEndpoints() throws Exception {
     String changeId = createChange().getChangeId();
-    execute(REVISION_ENDPOINTS, changeId, "current");
+    RestApiCallHelper.execute(adminRestSession, REVISION_ENDPOINTS, changeId, "current");
   }
 
   @Test
@@ -329,7 +333,8 @@
     AddReviewerInput addReviewerInput = new AddReviewerInput();
     addReviewerInput.reviewer = user.email;
 
-    execute(
+    RestApiCallHelper.execute(
+        adminRestSession,
         REVISION_REVIEWER_ENDPOINTS,
         () -> gApi.changes().id(changeId).addReviewer(addReviewerInput),
         changeId,
@@ -341,7 +346,8 @@
   public void revisionVoteEndpoints() throws Exception {
     String changeId = createChange().getChangeId();
 
-    execute(
+    RestApiCallHelper.execute(
+        adminRestSession,
         REVISION_VOTE_ENDPOINTS,
         () -> gApi.changes().id(changeId).current().review(ReviewInput.approve()),
         changeId,
@@ -362,7 +368,7 @@
       draftInput.message = "draft comment";
       CommentInfo draftInfo = gApi.changes().id(changeId).current().createDraft(draftInput).get();
 
-      execute(restCall, changeId, "current", draftInfo.id);
+      RestApiCallHelper.execute(adminRestSession, restCall, changeId, "current", draftInfo.id);
     }
   }
 
@@ -382,7 +388,7 @@
       reviewInput.drafts = DraftHandling.PUBLISH;
       gApi.changes().id(changeId).current().review(reviewInput);
 
-      execute(restCall, changeId, "current", commentInfo.id);
+      RestApiCallHelper.execute(adminRestSession, restCall, changeId, "current", commentInfo.id);
     }
   }
 
@@ -409,7 +415,8 @@
         gApi.changes().id(changeId).current().robotCommentsAsList();
     RobotCommentInfo robotCommentInfo = Iterables.getOnlyElement(robotCommentInfos);
 
-    execute(ROBOT_COMMENT_ENDPOINTS, changeId, "current", robotCommentInfo.id);
+    RestApiCallHelper.execute(
+        adminRestSession, ROBOT_COMMENT_ENDPOINTS, changeId, "current", robotCommentInfo.id);
   }
 
   @Test
@@ -449,13 +456,14 @@
     List<String> fixIds = getFixIds(robotCommentInfos);
     String fixId = Iterables.getOnlyElement(fixIds);
 
-    execute(FIX_ENDPOINTS, changeId, "current", fixId);
+    RestApiCallHelper.execute(adminRestSession, 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);
+    RestApiCallHelper.execute(
+        adminRestSession, REVISION_FILE_ENDPOINTS, changeId, "current", FILENAME);
   }
 
   @Test
@@ -465,7 +473,8 @@
     // A change message is created on change creation.
     String changeMessageId = Iterables.getOnlyElement(gApi.changes().id(changeId).messages()).id;
 
-    execute(CHANGE_MESSAGE_ENDPOINTS, changeId, changeMessageId);
+    RestApiCallHelper.execute(
+        adminRestSession, CHANGE_MESSAGE_ENDPOINTS, changeId, changeMessageId);
   }
 
   @Test
@@ -473,7 +482,8 @@
     String changeId = createChange("Subject", FILENAME, "content").getChangeId();
 
     // Each of the REST calls creates the change edit newly.
-    execute(
+    RestApiCallHelper.execute(
+        adminRestSession,
         CHANGE_EDIT_CREATE_ENDPOINTS,
         () -> adminRestSession.delete("/changes/" + changeId + "/edit"),
         changeId,
@@ -484,7 +494,7 @@
   public void changeEditEndpoints() throws Exception {
     String changeId = createChange("Subject", FILENAME, "content").getChangeId();
     gApi.changes().id(changeId).edit().create();
-    execute(CHANGE_EDIT_ENDPOINTS, changeId, FILENAME);
+    RestApiCallHelper.execute(adminRestSession, CHANGE_EDIT_ENDPOINTS, changeId, FILENAME);
   }
 
   private static Comment.Range createRange(
diff --git a/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
similarity index 87%
rename from javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
rename to javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
index 508d407..f187094 100644
--- a/javatests/com/google/gerrit/acceptance/rest/ConfigRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ConfigRestApiBindingsIT.java
@@ -12,13 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest;
+package com.google.gerrit.acceptance.rest.binding;
 
 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.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.restapi.config.ListTasks.TaskInfo;
@@ -31,10 +34,9 @@
  * 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}).
+ * the functionality of the config REST endpoints.
  */
-public class ConfigRestApiBindingsIT extends AbstractRestApiBindingsTest {
+public class ConfigRestApiBindingsIT extends AbstractDaemonTest {
   /**
    * Config REST endpoints to be tested, the URLs contain no placeholders since the only supported
    * config identifier ('server') can be hard-coded.
@@ -83,12 +85,12 @@
     // 'Access Database' is needed for the '/config/server/check.consistency' REST endpoint
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
 
-    execute(CONFIG_ENDPOINTS);
+    RestApiCallHelper.execute(adminRestSession, CONFIG_ENDPOINTS);
   }
 
   @Test
   public void cacheEndpoints() throws Exception {
-    execute(CACHE_ENDPOINTS, ProjectCacheImpl.CACHE_NAME);
+    RestApiCallHelper.execute(adminRestSession, CACHE_ENDPOINTS, ProjectCacheImpl.CACHE_NAME);
   }
 
   @Test
@@ -106,6 +108,6 @@
             .findFirst();
     assertThat(id).isPresent();
 
-    execute(TASK_ENDPOINTS, id.get());
+    RestApiCallHelper.execute(adminRestSession, TASK_ENDPOINTS, id.get());
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/GroupsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
similarity index 85%
rename from javatests/com/google/gerrit/acceptance/rest/GroupsRestApiBindingsIT.java
rename to javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
index 4538f75..b37bb01 100644
--- a/javatests/com/google/gerrit/acceptance/rest/GroupsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/GroupsRestApiBindingsIT.java
@@ -12,19 +12,21 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest;
+package com.google.gerrit.acceptance.rest.binding;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
 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}).
+ * functionality of the group REST endpoints.
  */
-public class GroupsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+public class GroupsRestApiBindingsIT extends AbstractDaemonTest {
   /**
    * Group REST endpoints to be tested, each URL contains a placeholder for the group identifier.
    */
@@ -80,14 +82,14 @@
   @Test
   public void groupEndpoints() throws Exception {
     String group = gApi.groups().create("test-group").get().name;
-    execute(GROUP_ENDPOINTS, group);
+    RestApiCallHelper.execute(adminRestSession, 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);
+    RestApiCallHelper.execute(adminRestSession, MEMBER_ENDPOINTS, group, admin.email);
   }
 
   @Test
@@ -95,6 +97,6 @@
     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);
+    RestApiCallHelper.execute(adminRestSession, SUBGROUP_ENDPOINTS, group, subgroup);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/PluginsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRestApiBindingsIT.java
similarity index 85%
rename from javatests/com/google/gerrit/acceptance/rest/PluginsRestApiBindingsIT.java
rename to javatests/com/google/gerrit/acceptance/rest/binding/PluginsRestApiBindingsIT.java
index 07ea3d0..5616ebc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/PluginsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/PluginsRestApiBindingsIT.java
@@ -12,12 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest;
+package com.google.gerrit.acceptance.rest.binding;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.extensions.api.plugins.InstallPluginInput;
 import com.google.gerrit.extensions.restapi.RawInput;
@@ -27,10 +30,9 @@
  * 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}).
+ * the functionality of the plugin REST endpoints.
  */
-public class PluginsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+public class PluginsRestApiBindingsIT extends AbstractDaemonTest {
   /**
    * Plugin REST endpoints to be tested, each URL contains a placeholder for the plugin identifier.
    */
@@ -57,7 +59,7 @@
   public void pluginEndpoints() throws Exception {
     String pluginName = "my-plugin";
     installPlugin(pluginName);
-    execute(PLUGIN_ENDPOINTS, pluginName);
+    RestApiCallHelper.execute(adminRestSession, PLUGIN_ENDPOINTS, pluginName);
   }
 
   private void installPlugin(String pluginName) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
similarity index 87%
rename from javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java
rename to javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index ca8d3ce..0c18dbb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -12,18 +12,21 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest;
+package com.google.gerrit.acceptance.rest.binding;
 
 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.acceptance.rest.util.RestCall.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_METHOD_NOT_ALLOWED;
 import static org.apache.http.HttpStatus.SC_NOT_FOUND;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -39,10 +42,9 @@
  * 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}).
+ * the functionality of the project REST endpoints.
  */
-public class ProjectsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+public class ProjectsRestApiBindingsIT extends AbstractDaemonTest {
   private static final ImmutableList<RestCall> PROJECT_ENDPOINTS =
       ImmutableList.of(
           RestCall.get("/projects/%s"),
@@ -157,49 +159,53 @@
 
   @Test
   public void projectEndpoints() throws Exception {
-    execute(PROJECT_ENDPOINTS, project.get());
+    RestApiCallHelper.execute(adminRestSession, 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());
+    RestApiCallHelper.execute(
+        adminRestSession, CHILD_PROJECT_ENDPOINTS, project.get(), childProject.get());
   }
 
   @Test
   public void branchEndpoints() throws Exception {
-    execute(BRANCH_ENDPOINTS, project.get(), "master");
+    RestApiCallHelper.execute(adminRestSession, BRANCH_ENDPOINTS, project.get(), "master");
   }
 
   @Test
   public void branchFileEndpoints() throws Exception {
     createAndSubmitChange(FILENAME);
-    execute(BRANCH_FILE_ENDPOINTS, project.get(), "master", FILENAME);
+    RestApiCallHelper.execute(
+        adminRestSession, BRANCH_FILE_ENDPOINTS, project.get(), "master", FILENAME);
   }
 
   @Test
   public void dashboardEndpoints() throws Exception {
     createDefaultDashboard();
-    execute(DASHBOARD_ENDPOINTS, project.get(), DEFAULT_DASHBOARD_NAME);
+    RestApiCallHelper.execute(
+        adminRestSession, 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);
+    RestApiCallHelper.execute(adminRestSession, TAG_ENDPOINTS, project.get(), tag);
   }
 
   @Test
   public void commitEndpoints() throws Exception {
     String commit = createAndSubmitChange(FILENAME);
-    execute(COMMIT_ENDPOINTS, project.get(), commit);
+    RestApiCallHelper.execute(adminRestSession, COMMIT_ENDPOINTS, project.get(), commit);
   }
 
   @Test
   public void commitFileEndpoints() throws Exception {
     String commit = createAndSubmitChange(FILENAME);
-    execute(COMMIT_FILE_ENDPOINTS, project.get(), commit, FILENAME);
+    RestApiCallHelper.execute(
+        adminRestSession, COMMIT_FILE_ENDPOINTS, project.get(), commit, FILENAME);
   }
 
   private String createAndSubmitChange(String filename) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/RootCollectionsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java
similarity index 79%
rename from javatests/com/google/gerrit/acceptance/rest/RootCollectionsRestApiBindingsIT.java
rename to javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java
index a2c4ea6..6d140c6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/RootCollectionsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/RootCollectionsRestApiBindingsIT.java
@@ -12,23 +12,25 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.acceptance.rest;
+package com.google.gerrit.acceptance.rest.binding;
 
-import static com.google.gerrit.acceptance.rest.AbstractRestApiBindingsTest.Method.GET;
+import static com.google.gerrit.acceptance.rest.util.RestApiCallHelper.execute;
+import static com.google.gerrit.acceptance.rest.util.RestCall.Method.GET;
 import static org.apache.http.HttpStatus.SC_NOT_FOUND;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.rest.util.RestCall;
 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}).
+ * functionality of the root REST endpoints.
  */
-public class RootCollectionsRestApiBindingsIT extends AbstractRestApiBindingsTest {
+public class RootCollectionsRestApiBindingsIT extends AbstractDaemonTest {
   /** Root REST endpoints to be tested, the URLs contain no placeholders. */
   private static final ImmutableList<RestCall> ROOT_ENDPOINTS =
       ImmutableList.of(
@@ -51,6 +53,6 @@
   @Test
   @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
   public void rootEndpoints() throws Exception {
-    execute(ROOT_ENDPOINTS);
+    execute(adminRestSession, ROOT_ENDPOINTS);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/BUILD b/javatests/com/google/gerrit/acceptance/rest/util/BUILD
new file mode 100644
index 0000000..115ea09
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/util/BUILD
@@ -0,0 +1,10 @@
+java_library(
+    name = "util",
+    testonly = 1,
+    srcs = glob(["*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/acceptance:lib",
+        "//lib/commons:lang",
+    ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
new file mode 100644
index 0000000..52e72fe
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/util/RestApiCallHelper.java
@@ -0,0 +1,104 @@
+// 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.util;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth.assert_;
+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_METHOD_NOT_ALLOWED;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import java.util.List;
+import org.junit.Ignore;
+
+/** Helper to execute REST API calls using the HTTP client. */
+@Ignore
+public class RestApiCallHelper {
+  /** @see #execute(RestSession, List, BeforeRestCall, String...) */
+  public static void execute(RestSession restSession, List<RestCall> restCalls, String... args)
+      throws Exception {
+    execute(restSession, restCalls, () -> {}, args);
+  }
+
+  /** @see #execute(RestSession, List, BeforeRestCall, String...) */
+  public static void execute(
+      RestSession restSession,
+      List<RestCall> restCalls,
+      BeforeRestCall beforeRestCall,
+      String... args)
+      throws Exception {
+    for (RestCall restCall : restCalls) {
+      beforeRestCall.run();
+      execute(restSession, restCall, args);
+    }
+  }
+
+  /**
+   * This method sends a request to a given 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.
+   */
+  public static void execute(RestSession restSession, RestCall restCall, String... args)
+      throws Exception {
+    String method = restCall.httpMethod().name();
+    String uri = restCall.uri(args);
+
+    RestResponse response;
+    switch (restCall.httpMethod()) {
+      case GET:
+        response = restSession.get(uri);
+        break;
+      case PUT:
+        response = restSession.put(uri);
+        break;
+      case POST:
+        response = restSession.post(uri);
+        break;
+      case DELETE:
+        response = restSession.delete(uri);
+        break;
+      default:
+        assert_().fail(String.format("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());
+      if (restCall.expectedMessage().isPresent()) {
+        assertWithMessage(msg).that(body).contains(restCall.expectedMessage().get());
+      }
+    } else {
+      assertWithMessage(msg)
+          .that(status)
+          .isNotIn(ImmutableList.of(SC_FORBIDDEN, SC_NOT_FOUND, SC_METHOD_NOT_ALLOWED));
+      assertWithMessage(msg).that(status).isLessThan(SC_INTERNAL_SERVER_ERROR);
+    }
+  }
+
+  @FunctionalInterface
+  public interface BeforeRestCall {
+    void run() throws Exception;
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java b/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java
new file mode 100644
index 0000000..a322089
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/util/RestCall.java
@@ -0,0 +1,88 @@
+// 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.util;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auto.value.AutoValue;
+import java.util.Optional;
+import jdk.nashorn.internal.ir.annotations.Ignore;
+import org.apache.commons.lang.StringUtils;
+
+/** Data container for test REST requests. */
+@Ignore
+@AutoValue
+public abstract class RestCall {
+  public enum Method {
+    GET,
+    PUT,
+    POST,
+    DELETE
+  }
+
+  public static RestCall get(String uriFormat) {
+    return builder(Method.GET, uriFormat).build();
+  }
+
+  public static RestCall put(String uriFormat) {
+    return builder(Method.PUT, uriFormat).build();
+  }
+
+  public static RestCall post(String uriFormat) {
+    return builder(Method.POST, uriFormat).build();
+  }
+
+  public static RestCall delete(String uriFormat) {
+    return builder(Method.DELETE, uriFormat).build();
+  }
+
+  public static Builder builder(Method httpMethod, String uriFormat) {
+    return new AutoValue_RestCall.Builder().httpMethod(httpMethod).uriFormat(uriFormat);
+  }
+
+  public abstract Method httpMethod();
+
+  public abstract String uriFormat();
+
+  public abstract Optional<Integer> expectedResponseCode();
+
+  public abstract Optional<String> expectedMessage();
+
+  public 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
+  public abstract static class Builder {
+    public abstract Builder httpMethod(Method httpMethod);
+
+    public abstract Builder uriFormat(String uriFormat);
+
+    public abstract Builder expectedResponseCode(int expectedResponseCode);
+
+    public abstract Builder expectedMessage(String expectedMessage);
+
+    public abstract RestCall build();
+  }
+}