Merge branch 'stable-3.4' into master

* stable-3.4:
  Allow using pull-replication on replica nodes

Change-Id: Ic051cddcc98e0d10a1ba926cd5a3a1246e05b4fd
diff --git a/BUILD b/BUILD
index 705c332..82112d1 100644
--- a/BUILD
+++ b/BUILD
@@ -10,6 +10,7 @@
         "Gerrit-PluginName: pull-replication",
         "Gerrit-Module: com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
         "Gerrit-SshModule: com.googlesource.gerrit.plugins.replication.pull.SshModule",
+        "Gerrit-HttpModule: com.googlesource.gerrit.plugins.replication.pull.api.HttpModule"
     ],
     resources = glob(["src/main/resources/**/*"]),
     deps = [
@@ -45,5 +46,6 @@
     ),
     deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
         ":pull-replication__plugin",
+         "//plugins/replication:replication",
     ],
 )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
new file mode 100644
index 0000000..7ae7d54
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 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.googlesource.gerrit.plugins.replication.pull.api;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.httpd.AllRequestFilter;
+import com.google.gerrit.server.config.GerritIsReplica;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import com.google.inject.servlet.ServletModule;
+
+public class HttpModule extends ServletModule {
+  private boolean isReplica;
+
+  @Inject
+  public HttpModule(@GerritIsReplica Boolean isReplica) {
+    this.isReplica = isReplica;
+  }
+
+  @Override
+  protected void configureServlets() {
+    if (isReplica) {
+      DynamicSet.bind(binder(), AllRequestFilter.class)
+          .to(PullReplicationFilter.class)
+          .in(Scopes.SINGLETON);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
new file mode 100644
index 0000000..65b8e1b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -0,0 +1,225 @@
+// Copyright (C) 2021 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.googlesource.gerrit.plugins.replication.pull.api;
+
+import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_CREATED;
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.Splitter;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.httpd.AllRequestFilter;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.ProjectsCollection;
+import com.google.gson.Gson;
+import com.google.gson.JsonParseException;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.MalformedJsonException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
+import java.io.BufferedReader;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class PullReplicationFilter extends AllRequestFilter {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private FetchAction fetchAction;
+  private ApplyObjectAction applyObjectAction;
+  private ProjectsCollection projectsCollection;
+  private Gson gson;
+  private Provider<CurrentUser> userProvider;
+
+  @Inject
+  public PullReplicationFilter(
+      FetchAction fetchAction,
+      ApplyObjectAction applyObjectAction,
+      ProjectsCollection projectsCollection,
+      Provider<CurrentUser> userProvider) {
+    this.fetchAction = fetchAction;
+    this.applyObjectAction = applyObjectAction;
+    this.projectsCollection = projectsCollection;
+    this.userProvider = userProvider;
+    this.gson = OutputFormat.JSON.newGsonBuilder().create();
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+    if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
+      chain.doFilter(request, response);
+      return;
+    }
+
+    HttpServletResponse httpResponse = (HttpServletResponse) response;
+    HttpServletRequest httpRequest = (HttpServletRequest) request;
+    try {
+      if (isFetchAction(httpRequest)) {
+        if (userProvider.get().isIdentifiedUser()) {
+          writeResponse(httpResponse, doFetch(httpRequest));
+        } else {
+          httpResponse.sendError(SC_UNAUTHORIZED);
+        }
+      } else if (isApplyObjectAction(httpRequest)) {
+        if (userProvider.get().isIdentifiedUser()) {
+          writeResponse(httpResponse, doApplyObject(httpRequest));
+        } else {
+          httpResponse.sendError(SC_UNAUTHORIZED);
+        }
+      } else {
+        chain.doFilter(request, response);
+      }
+
+    } catch (AuthException e) {
+      RestApiServlet.replyError(
+          httpRequest, httpResponse, SC_FORBIDDEN, e.getMessage(), e.caching(), e);
+    } catch (MalformedJsonException | JsonParseException e) {
+      logger.atFine().withCause(e).log("REST call failed on JSON parsing");
+      RestApiServlet.replyError(
+          httpRequest, httpResponse, SC_BAD_REQUEST, "Invalid json in request", e);
+    } catch (BadRequestException e) {
+      RestApiServlet.replyError(httpRequest, httpResponse, SC_BAD_REQUEST, e.getMessage(), e);
+    } catch (UnprocessableEntityException e) {
+      RestApiServlet.replyError(
+          httpRequest, httpResponse, SC_UNPROCESSABLE_ENTITY, e.getMessage(), e.caching(), e);
+    } catch (ResourceConflictException e) {
+      RestApiServlet.replyError(
+          httpRequest, httpResponse, SC_CONFLICT, e.getMessage(), e.caching(), e);
+    } catch (Exception e) {
+      throw new ServletException(e);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private Response<Map<String, Object>> doApplyObject(HttpServletRequest httpRequest)
+      throws RestApiException, IOException, PermissionBackendException {
+    RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class));
+    IdString id = getProjectName(httpRequest);
+    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
+
+    return (Response<Map<String, Object>>) applyObjectAction.apply(projectResource, input);
+  }
+
+  @SuppressWarnings("unchecked")
+  private Response<Map<String, Object>> doFetch(HttpServletRequest httpRequest)
+      throws IOException, RestApiException, PermissionBackendException {
+    Input input = readJson(httpRequest, TypeLiteral.get(Input.class));
+    IdString id = getProjectName(httpRequest);
+    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
+
+    return (Response<Map<String, Object>>) fetchAction.apply(projectResource, input);
+  }
+
+  private void writeResponse(
+      HttpServletResponse httpResponse, Response<Map<String, Object>> response) throws IOException {
+    String responseJson = gson.toJson(response);
+    if (response.statusCode() == SC_OK || response.statusCode() == SC_CREATED) {
+
+      httpResponse.setContentType("application/json");
+      httpResponse.setStatus(response.statusCode());
+      PrintWriter writer = httpResponse.getWriter();
+      writer.print(new String(RestApiServlet.JSON_MAGIC));
+      writer.print(responseJson);
+    } else {
+      httpResponse.sendError(response.statusCode(), responseJson);
+    }
+  }
+
+  private <T> T readJson(HttpServletRequest httpRequest, TypeLiteral<T> typeLiteral)
+      throws IOException, BadRequestException {
+
+    try (BufferedReader br = httpRequest.getReader();
+        JsonReader json = new JsonReader(br)) {
+      try {
+        json.setLenient(true);
+
+        try {
+          json.peek();
+        } catch (EOFException e) {
+          throw new BadRequestException("Expected JSON object", e);
+        }
+
+        return gson.fromJson(json, typeLiteral.getType());
+      } finally {
+        try {
+          // Reader.close won't consume the rest of the input. Explicitly consume the request
+          // body.
+          br.skip(Long.MAX_VALUE);
+        } catch (Exception e) {
+          // ignore, e.g. trying to consume the rest of the input may fail if the request was
+          // cancelled
+          logger.atFine().withCause(e).log("Exception during the parsing of the request json");
+        }
+      }
+    }
+  }
+
+  /**
+   * Return project name from request URI. Request URI format:
+   * /a/projects/<project_name>/pull-replication~apply-object
+   *
+   * @param req
+   * @return project name
+   */
+  private IdString getProjectName(HttpServletRequest req) {
+    String path = req.getRequestURI();
+
+    List<IdString> out = new ArrayList<>();
+    for (String p : Splitter.on('/').split(path)) {
+      out.add(IdString.fromUrl(p));
+    }
+    if (!out.isEmpty() && out.get(out.size() - 1).isEmpty()) {
+      out.remove(out.size() - 1);
+    }
+    return out.get(3);
+  }
+
+  private boolean isApplyObjectAction(HttpServletRequest httpRequest) {
+    return httpRequest.getRequestURI().endsWith("pull-replication~apply-object");
+  }
+
+  private boolean isFetchAction(HttpServletRequest httpRequest) {
+    return httpRequest.getRequestURI().endsWith("pull-replication~fetch");
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
new file mode 100644
index 0000000..49cf608
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
@@ -0,0 +1,202 @@
+// Copyright (C) 2021 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.googlesource.gerrit.plugins.replication.pull.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
+import com.googlesource.gerrit.plugins.replication.pull.RevisionReader;
+import com.googlesource.gerrit.plugins.replication.pull.Source;
+import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
+import com.googlesource.gerrit.plugins.replication.pull.client.SourceHttpClient;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Optional;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.message.BasicHeader;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+@SkipProjectClone
+@UseLocalDisk
+@TestPlugin(
+    name = "pull-replication",
+    sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
+    httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
+public abstract class ActionITBase extends LightweightPluginDaemonTest {
+  protected static final Optional<String> ALL_PROJECTS = Optional.empty();
+  protected static final int TEST_REPLICATION_DELAY = 60;
+  protected static final String TEST_REPLICATION_SUFFIX = "suffix1";
+  protected static final String TEST_REPLICATION_REMOTE = "remote1";
+
+  protected Path gitPath;
+  protected FileBasedConfig config;
+  protected FileBasedConfig secureConfig;
+  protected RevisionReader revisionReader;
+
+  @Inject private SitePaths sitePaths;
+  @Inject private ProjectOperations projectOperations;
+  CredentialsFactory credentials;
+  Source source;
+  SourceHttpClient.Factory httpClientFactory;
+  String url;
+
+  protected abstract String getURL();
+
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    gitPath = sitePaths.site_path.resolve("git");
+
+    config =
+        new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED);
+    setReplicationSource(
+        TEST_REPLICATION_REMOTE,
+        TEST_REPLICATION_SUFFIX,
+        ALL_PROJECTS); // Simulates a full replication.config initialization
+    config.save();
+
+    secureConfig =
+        new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED);
+    setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword());
+    secureConfig.save();
+
+    super.setUpTestPlugin();
+
+    httpClientFactory = plugin.getSysInjector().getInstance(SourceHttpClient.Factory.class);
+    credentials = plugin.getSysInjector().getInstance(CredentialsFactory.class);
+    revisionReader = plugin.getSysInjector().getInstance(RevisionReader.class);
+    source = plugin.getSysInjector().getInstance(SourcesCollection.class).getAll().get(0);
+
+    url = getURL();
+  }
+
+  protected HttpPost createRequest(String sendObjectPayload) {
+    HttpPost post = new HttpPost(url);
+    post.setEntity(new StringEntity(sendObjectPayload, StandardCharsets.UTF_8));
+    post.addHeader(new BasicHeader("Content-Type", "application/json"));
+    return post;
+  }
+
+  protected String createRef() throws Exception {
+    return createRef(Project.nameKey(project + TEST_REPLICATION_SUFFIX));
+  }
+
+  protected String createRef(NameKey projectName) throws Exception {
+    testRepo = cloneProject(createTestProject(projectName.get()));
+
+    Result pushResult = createChange("topic", "test.txt", "test_content");
+    return RefNames.changeMetaRef(pushResult.getChange().getId());
+  }
+
+  protected Optional<RevisionData> createRevisionData(String refName) throws Exception {
+    return createRevisionData(Project.nameKey(project + TEST_REPLICATION_SUFFIX), refName);
+  }
+
+  protected Optional<RevisionData> createRevisionData(NameKey projectName, String refName)
+      throws Exception {
+    return revisionReader.read(projectName, refName);
+  }
+
+  protected Object encode(byte[] content) {
+    return Base64.getEncoder().encodeToString(content);
+  }
+
+  public ResponseHandler<Object> assertHttpResponseCode(int responseCode) {
+    return new ResponseHandler<Object>() {
+
+      @Override
+      public Object handleResponse(HttpResponse response)
+          throws ClientProtocolException, IOException {
+        assertThat(response.getStatusLine().getStatusCode()).isEqualTo(responseCode);
+        return null;
+      }
+    };
+  }
+
+  protected HttpClientContext getContext() {
+    HttpClientContext ctx = HttpClientContext.create();
+    CredentialsProvider adapted = new BasicCredentialsProvider();
+    adapted.setCredentials(
+        AuthScope.ANY, new UsernamePasswordCredentials(admin.username(), admin.httpPassword()));
+    ctx.setCredentialsProvider(adapted);
+    return ctx;
+  }
+
+  protected HttpClientContext getAnonymousContext() {
+    HttpClientContext ctx = HttpClientContext.create();
+    return ctx;
+  }
+
+  private Project.NameKey createTestProject(String name) throws Exception {
+    return projectOperations.newProject().name(name).parent(project).create();
+  }
+
+  private void setReplicationSource(
+      String remoteName, String replicaSuffix, Optional<String> project) throws IOException {
+    setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project);
+  }
+
+  private void setReplicationSource(
+      String remoteName, List<String> replicaSuffixes, Optional<String> project)
+      throws IOException {
+
+    List<String> replicaUrls =
+        replicaSuffixes.stream()
+            .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString())
+            .collect(toList());
+    config.setString("replication", null, "instanceLabel", remoteName);
+    config.setStringList("remote", remoteName, "url", replicaUrls);
+    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
+    config.setString("remote", remoteName, "fetch", "+refs/tags/*:refs/tags/*");
+    config.setInt("remote", remoteName, "timeout", 600);
+    config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY);
+    project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj));
+    config.setBoolean("gerrit", null, "autoReload", true);
+    config.save();
+  }
+
+  private void setReplicationCredentials(String remoteName, String username, String password)
+      throws IOException {
+    secureConfig.setString("remote", remoteName, "username", username);
+    secureConfig.setString("remote", remoteName, "password", password);
+    secureConfig.save();
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java
index 75c31de..ec172ad 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java
@@ -15,99 +15,17 @@
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
 import static com.google.common.truth.Truth.assertThat;
-import static java.util.stream.Collectors.toList;
 
-import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit.Result;
-import com.google.gerrit.acceptance.SkipProjectClone;
-import com.google.gerrit.acceptance.TestPlugin;
-import com.google.gerrit.acceptance.UseLocalDisk;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
-import com.googlesource.gerrit.plugins.replication.pull.RevisionReader;
-import com.googlesource.gerrit.plugins.replication.pull.Source;
-import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
-import com.googlesource.gerrit.plugins.replication.pull.client.SourceHttpClient;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Base64;
-import java.util.List;
 import java.util.Optional;
-import org.apache.http.HttpResponse;
-import org.apache.http.auth.AuthScope;
-import org.apache.http.auth.UsernamePasswordCredentials;
-import org.apache.http.client.ClientProtocolException;
-import org.apache.http.client.CredentialsProvider;
-import org.apache.http.client.ResponseHandler;
 import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.protocol.HttpClientContext;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.BasicCredentialsProvider;
-import org.apache.http.message.BasicHeader;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
-@SkipProjectClone
-@UseLocalDisk
-@TestPlugin(
-    name = "pull-replication",
-    sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule")
-public class ApplyObjectActionIT extends LightweightPluginDaemonTest {
-  private static final Optional<String> ALL_PROJECTS = Optional.empty();
-  private static final int TEST_REPLICATION_DELAY = 60;
-  private static final String TEST_REPLICATION_SUFFIX = "suffix1";
-  private static final String TEST_REPLICATION_REMOTE = "remote1";
-
-  private Path gitPath;
-  private FileBasedConfig config;
-  private FileBasedConfig secureConfig;
-  private RevisionReader revisionReader;
-
-  @Inject private SitePaths sitePaths;
-  @Inject private ProjectOperations projectOperations;
-  CredentialsFactory credentials;
-  Source source;
-  SourceHttpClient.Factory httpClientFactory;
-  String url;
-
-  @Override
-  public void setUpTestPlugin() throws Exception {
-    gitPath = sitePaths.site_path.resolve("git");
-
-    config =
-        new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED);
-    setReplicationSource(
-        TEST_REPLICATION_REMOTE,
-        TEST_REPLICATION_SUFFIX,
-        ALL_PROJECTS); // Simulates a full replication.config initialization
-    config.save();
-
-    secureConfig =
-        new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED);
-    setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword());
-    secureConfig.save();
-
-    super.setUpTestPlugin();
-
-    httpClientFactory = plugin.getSysInjector().getInstance(SourceHttpClient.Factory.class);
-    credentials = plugin.getSysInjector().getInstance(CredentialsFactory.class);
-    revisionReader = plugin.getSysInjector().getInstance(RevisionReader.class);
-    source = plugin.getSysInjector().getInstance(SourcesCollection.class).getAll().get(0);
-
-    url =
-        String.format(
-            "%s/a/projects/%s/pull-replication~apply-object",
-            adminRestSession.url(), Url.encode(project.get()));
-  }
+public class ApplyObjectActionIT extends ActionITBase {
 
   @Test
   public void shouldAcceptPayloadWithAsyncField() throws Exception {
@@ -147,6 +65,71 @@
   }
 
   @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldAcceptPayloadWhenNodeIsAReplica() throws Exception {
+    String payloadWithoutAsyncFieldTemplate =
+        "{\"label\":\""
+            + TEST_REPLICATION_REMOTE
+            + "\",\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}}";
+
+    String refName = createRef();
+    Optional<RevisionData> revisionDataOption = createRevisionData(refName);
+    assertThat(revisionDataOption.isPresent()).isTrue();
+
+    RevisionData revisionData = revisionDataOption.get();
+    String sendObjectPayload =
+        createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData);
+
+    HttpPost post = createRequest(sendObjectPayload);
+    httpClientFactory.create(source).execute(post, assertHttpResponseCode(201), getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldAcceptPayloadWhenNodeIsAReplicaAndProjectNameContainsSlash() throws Exception {
+    String payloadWithoutAsyncFieldTemplate =
+        "{\"label\":\""
+            + TEST_REPLICATION_REMOTE
+            + "\",\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}}";
+    NameKey projectName = Project.nameKey("test/repo");
+    String refName = createRef(projectName);
+    Optional<RevisionData> revisionDataOption = createRevisionData(projectName, refName);
+    assertThat(revisionDataOption.isPresent()).isTrue();
+
+    RevisionData revisionData = revisionDataOption.get();
+    String sendObjectPayload =
+        createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData);
+    url =
+        String.format(
+            "%s/a/projects/%s/pull-replication~apply-object",
+            adminRestSession.url(), Url.encode(projectName.get()));
+    HttpPost post = createRequest(sendObjectPayload);
+    httpClientFactory.create(source).execute(post, assertHttpResponseCode(201), getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnUnauthorizedWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception {
+    String payloadWithoutAsyncFieldTemplate =
+        "{\"label\":\""
+            + TEST_REPLICATION_REMOTE
+            + "\",\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}}";
+
+    String refName = createRef();
+    Optional<RevisionData> revisionDataOption = createRevisionData(refName);
+    assertThat(revisionDataOption.isPresent()).isTrue();
+
+    RevisionData revisionData = revisionDataOption.get();
+    String sendObjectPayload =
+        createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData);
+
+    HttpPost post = createRequest(sendObjectPayload);
+    httpClientFactory
+        .create(source)
+        .execute(post, assertHttpResponseCode(401), getAnonymousContext());
+  }
+
+  @Test
   public void shouldReturnBadRequestCodeWhenMandatoryFieldLabelIsMissing() throws Exception {
     String payloadWithoutLabelFieldTemplate =
         "{\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}, \"async\":true}";
@@ -192,81 +175,10 @@
     return sendObjectPayload;
   }
 
-  private HttpPost createRequest(String sendObjectPayload) {
-    HttpPost post = new HttpPost(url);
-    post.setEntity(new StringEntity(sendObjectPayload, StandardCharsets.UTF_8));
-    post.addHeader(new BasicHeader("Content-Type", "application/json"));
-    return post;
-  }
-
-  private String createRef() throws Exception {
-    testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
-
-    Result pushResult = createChange("topic", "test.txt", "test_content");
-    return RefNames.changeMetaRef(pushResult.getChange().getId());
-  }
-
-  private Optional<RevisionData> createRevisionData(String refName) throws Exception {
-    return revisionReader.read(Project.nameKey(project + TEST_REPLICATION_SUFFIX), refName);
-  }
-
-  private Object encode(byte[] content) {
-    return Base64.getEncoder().encodeToString(content);
-  }
-
-  public ResponseHandler<Object> assertHttpResponseCode(int responseCode) {
-    return new ResponseHandler<Object>() {
-
-      @Override
-      public Object handleResponse(HttpResponse response)
-          throws ClientProtocolException, IOException {
-        assertThat(response.getStatusLine().getStatusCode()).isEqualTo(responseCode);
-        return null;
-      }
-    };
-  }
-
-  private HttpClientContext getContext() {
-    HttpClientContext ctx = HttpClientContext.create();
-    CredentialsProvider adapted = new BasicCredentialsProvider();
-    adapted.setCredentials(
-        AuthScope.ANY, new UsernamePasswordCredentials(admin.username(), admin.httpPassword()));
-    ctx.setCredentialsProvider(adapted);
-    return ctx;
-  }
-
-  private Project.NameKey createTestProject(String name) throws Exception {
-    return projectOperations.newProject().name(name).parent(project).create();
-  }
-
-  private void setReplicationSource(
-      String remoteName, String replicaSuffix, Optional<String> project) throws IOException {
-    setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project);
-  }
-
-  private void setReplicationSource(
-      String remoteName, List<String> replicaSuffixes, Optional<String> project)
-      throws IOException {
-
-    List<String> replicaUrls =
-        replicaSuffixes.stream()
-            .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString())
-            .collect(toList());
-    config.setString("replication", null, "instanceLabel", remoteName);
-    config.setStringList("remote", remoteName, "url", replicaUrls);
-    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
-    config.setString("remote", remoteName, "fetch", "+refs/tags/*:refs/tags/*");
-    config.setInt("remote", remoteName, "timeout", 600);
-    config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY);
-    project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj));
-    config.setBoolean("gerrit", null, "autoReload", true);
-    config.save();
-  }
-
-  private void setReplicationCredentials(String remoteName, String username, String password)
-      throws IOException {
-    secureConfig.setString("remote", remoteName, "username", username);
-    secureConfig.setString("remote", remoteName, "password", password);
-    secureConfig.save();
+  @Override
+  protected String getURL() {
+    return String.format(
+        "%s/a/projects/%s/pull-replication~apply-object",
+        adminRestSession.url(), Url.encode(project.get()));
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java
new file mode 100644
index 0000000..7b73df7
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2021 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.googlesource.gerrit.plugins.replication.pull.api;
+
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.restapi.Url;
+import org.junit.Test;
+
+public class FetchActionIT extends ActionITBase {
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldFetchRefWhenNodeIsAReplica() throws Exception {
+    String refName = createRef();
+    String sendObjectPayload =
+        "{\"label\":\""
+            + TEST_REPLICATION_REMOTE
+            + "\", \"ref_name\": \""
+            + refName
+            + "\", \"async\":false}";
+
+    httpClientFactory
+        .create(source)
+        .execute(createRequest(sendObjectPayload), assertHttpResponseCode(201), getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldFetchRefWhenNodeIsAReplicaAndProjectNameContainsSlash() throws Exception {
+    NameKey projectName = Project.nameKey("test/repo");
+    String refName = createRef(projectName);
+    String sendObjectPayload =
+        "{\"label\":\""
+            + TEST_REPLICATION_REMOTE
+            + "\", \"ref_name\": \""
+            + refName
+            + "\", \"async\":false}";
+    url =
+        String.format(
+            "%s/a/projects/%s/pull-replication~fetch",
+            adminRestSession.url(), Url.encode(projectName.get()));
+    httpClientFactory
+        .create(source)
+        .execute(createRequest(sendObjectPayload), assertHttpResponseCode(201), getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnUnauthorizedWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception {
+    String refName = createRef();
+    String sendObjectPayload =
+        "{\"label\":\""
+            + TEST_REPLICATION_REMOTE
+            + "\", \"ref_name\": \""
+            + refName
+            + "\", \"async\":false}";
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createRequest(sendObjectPayload), assertHttpResponseCode(401), getAnonymousContext());
+  }
+
+  @Override
+  protected String getURL() {
+    return String.format(
+        "%s/a/projects/%s/pull-replication~fetch",
+        adminRestSession.url(), Url.encode(project.get()));
+  }
+}