Merge branch 'stable-3.5' into stable-3.6

* stable-3.5:
  Add missing local JGit fetch test assertion
  Fix typo in test names
  Do not rely on async/wait for synchronous fetch replication
  Make sure that the EventListener receives replication events
  Add mirror replication option for JGit client
  Remove unnecessary checked exception
  Cover the replication failure scenario and fix the metrics
  Remove white-box unit tests on synchronous FetchCommand
  Throw Exception from tests

Change-Id: Ib51ef135049a2fceed1246886ce22efdd1a67c8a
diff --git a/example-setup/broker/Dockerfile b/example-setup/broker/Dockerfile
index 08eaba9..b79470c 100644
--- a/example-setup/broker/Dockerfile
+++ b/example-setup/broker/Dockerfile
@@ -1,4 +1,4 @@
-FROM gerritcodereview/gerrit:3.5.5-almalinux8
+FROM gerritcodereview/gerrit:3.6.3-almalinux8
 
 USER root
 
@@ -12,7 +12,7 @@
 # hence rename it with a 'z-' prefix because the Gerrit plugin loader starts the
 # plugins in filename alphabetical order.
 COPY --chown=gerrit:gerrit events-kafka.jar /var/gerrit/plugins/z-events-kafka.jar
-COPY --chown=gerrit:gerrit libevents-broker.jar /var/gerrit/lib/libevents-broker.jar
+COPY --chown=gerrit:gerrit events-broker.jar /var/gerrit/lib/events-broker.jar
 
 COPY --chown=gerrit:gerrit entrypoint.sh /tmp/
 COPY --chown=gerrit:gerrit configs/replication.config.template /var/gerrit/etc/
diff --git a/example-setup/http/Dockerfile b/example-setup/http/Dockerfile
index e9f8239..77fed72 100644
--- a/example-setup/http/Dockerfile
+++ b/example-setup/http/Dockerfile
@@ -1,4 +1,4 @@
-FROM gerritcodereview/gerrit:3.5.5-almalinux8
+FROM gerritcodereview/gerrit:3.6.3-almalinux8
 
 USER root
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
index 370f2fb..d02cb02 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
@@ -41,6 +41,7 @@
 import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -336,23 +337,33 @@
     try {
       long startedAt = context.getStartTime();
       long delay = NANOSECONDS.toMillis(startedAt - createdAt);
-      metrics.record(config.getName(), delay, retryCount);
       git = gitManager.openRepository(projectName);
-      runImpl();
-      long elapsed = NANOSECONDS.toMillis(context.stop());
-      Optional<Long> elapsedEnd2End =
-          apiRequestMetrics
-              .flatMap(metrics -> metrics.stop(config.getName()))
-              .map(NANOSECONDS::toMillis);
-      repLog.info(
-          "[{}] {} replication from {} completed in {}ms, {}ms delay, {} retries{}",
-          taskIdHex,
-          replicationType,
-          uri,
-          elapsed,
-          delay,
-          retryCount,
-          elapsedEnd2End.map(el -> String.format(", E2E %dms", el)).orElse(""));
+      List<RefSpec> fetchRefSpecs = runImpl();
+
+      if (fetchRefSpecs.isEmpty()) {
+        repLog.info(
+            "[{}] {} replication from {} finished but no refs were replicated, {}ms delay, {} retries",
+            taskIdHex,
+            replicationType,
+            uri,
+            delay,
+            retryCount);
+      } else {
+        metrics.record(config.getName(), delay, retryCount);
+        long elapsed = NANOSECONDS.toMillis(context.stop());
+        Optional<Long> elapsedEnd2End =
+            apiRequestMetrics
+                .flatMap(metrics -> metrics.stop(config.getName()))
+                .map(NANOSECONDS::toMillis);
+        repLog.info(
+            "[{}] Replication from {} completed in {}ms, {}ms delay, {} retries{}",
+            taskIdHex,
+            uri,
+            elapsed,
+            delay,
+            retryCount,
+            elapsedEnd2End.map(el -> String.format(", E2E %dms", el)).orElse(""));
+      }
     } catch (RepositoryNotFoundException e) {
       stateLog.error(
           "["
@@ -428,7 +439,7 @@
     repLog.info("[{}] Cannot replicate from {}. It was canceled while running", taskIdHex, uri, e);
   }
 
-  private void runImpl() throws IOException {
+  private List<RefSpec> runImpl() throws IOException {
     Fetch fetch = fetchFactory.create(taskIdHex, uri, git);
     List<RefSpec> fetchRefSpecs = getFetchRefSpecs();
 
@@ -445,7 +456,7 @@
       delta.remove(inexistentRef);
       if (delta.isEmpty()) {
         repLog.warn("[{}] Empty replication task, skipping.", taskIdHex);
-        return;
+        return Collections.emptyList();
       }
 
       runImpl();
@@ -453,6 +464,7 @@
       notifyRefReplicatedIOException();
       throw e;
     }
+    return fetchRefSpecs;
   }
 
   public List<RefSpec> getFetchRefSpecs() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/OnStartStop.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/OnStartStop.java
index d8c4a8d..6457f8a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/OnStartStop.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/OnStartStop.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.server.config.GerritIsReplica;
 import com.google.gerrit.server.events.EventDispatcher;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
@@ -36,6 +37,7 @@
   private final ReplicationState.Factory replicationStateFactory;
   private final SourcesCollection sourcesCollection;
   private final WorkQueue workQueue;
+  private boolean isReplica;
 
   @Inject
   protected OnStartStop(
@@ -45,7 +47,8 @@
       DynamicItem<EventDispatcher> eventDispatcher,
       ReplicationState.Factory replicationStateFactory,
       SourcesCollection sourcesCollection,
-      WorkQueue workQueue) {
+      WorkQueue workQueue,
+      @GerritIsReplica Boolean isReplica) {
     this.srvInfo = srvInfo;
     this.fetchAll = fetchAll;
     this.config = config;
@@ -54,11 +57,13 @@
     this.fetchAllFuture = Atomics.newReference();
     this.sourcesCollection = sourcesCollection;
     this.workQueue = workQueue;
+    this.isReplica = isReplica;
   }
 
   @Override
   public void start() {
-    if (srvInfo.getState() == ServerInformation.State.STARTUP
+    if (isReplica
+        && srvInfo.getState() == ServerInformation.State.STARTUP
         && config.isReplicateAllOnPluginStart()) {
       ReplicationState state =
           replicationStateFactory.create(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
index 72a6b74..005d383 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.replication.pull;
 
 import static com.googlesource.gerrit.plugins.replication.StartReplicationCapability.START_REPLICATION;
+import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION;
 
 import com.google.common.eventbus.EventBus;
 import com.google.gerrit.extensions.annotations.Exports;
@@ -44,8 +45,8 @@
 import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
 import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
 import com.googlesource.gerrit.plugins.replication.StartReplicationCapability;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
-import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiModule;
 import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule;
 import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
 import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
@@ -82,13 +83,16 @@
         .annotatedWith(Names.named(ReplicationQueueMetrics.REPLICATION_QUEUE_METRICS))
         .toInstance(pluginMetricMaker);
 
+    bind(CapabilityDefinition.class)
+        .annotatedWith(Exports.named(CALL_FETCH_ACTION))
+        .to(FetchApiCapability.class);
+
     install(new PullReplicationGroupModule());
     bind(BearerTokenProvider.class).in(Scopes.SINGLETON);
     bind(RevisionReader.class).in(Scopes.SINGLETON);
     bind(ApplyObject.class);
     install(new FactoryModuleBuilder().build(FetchJob.Factory.class));
     install(new ApplyObjectCacheModule());
-    install(new PullReplicationApiModule());
 
     install(new FetchRefReplicatedEventModule());
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
index 6bafaa4..1ba47de 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
@@ -181,12 +181,7 @@
             event.getRefName(),
             event.refUpdate.get().oldRev,
             event.refUpdate.get().newRev);
-        fire(
-            event.refUpdate.get().project,
-            ObjectId.fromString(event.refUpdate.get().newRev),
-            event.getRefName(),
-            event.eventCreatedOn,
-            ZEROS_OBJECTID.equals(event.refUpdate.get().newRev));
+        fire(ReferenceUpdatedEvent.from(event));
       }
     }
   }
@@ -215,42 +210,42 @@
     return !refsFilter.match(refName);
   }
 
-  private void fire(
-      String projectName,
-      ObjectId objectId,
-      String refName,
-      long eventCreatedOn,
-      boolean isDelete) {
+  private void fire(ReferenceUpdatedEvent event) {
     ReplicationState state = new ReplicationState(new GitUpdateProcessing(dispatcher.get()));
-    fire(Project.nameKey(projectName), objectId, refName, eventCreatedOn, isDelete, state);
+    fire(event, state);
     state.markAllFetchTasksScheduled();
   }
 
-  private void fire(
-      NameKey project,
-      ObjectId objectId,
-      String refName,
-      long eventCreatedOn,
-      boolean isDelete,
-      ReplicationState state) {
+  private void fire(ReferenceUpdatedEvent event, ReplicationState state) {
     if (!running) {
       stateLog.warn(
           "Replication plugin did not finish startup before event, event replication is postponed",
           state);
-      beforeStartupEventsQueue.add(
-          ReferenceUpdatedEvent.create(project.get(), refName, objectId, eventCreatedOn, isDelete));
+      beforeStartupEventsQueue.add(event);
 
       queueMetrics.incrementQueuedBeforStartup();
       return;
     }
     ForkJoinPool fetchCallsPool = null;
     try {
-      fetchCallsPool = new ForkJoinPool(sources.get().getAll().size());
+      List<Source> allSources = sources.get().getAll();
+      int numSources = allSources.size();
+      if (numSources == 0) {
+        repLog.debug("No replication sources configured -> skipping fetch");
+        return;
+      }
+      fetchCallsPool = new ForkJoinPool(numSources);
 
       final Consumer<Source> callFunction =
-          callFunction(project, objectId, refName, eventCreatedOn, isDelete, state);
+          callFunction(
+              Project.nameKey(event.projectName()),
+              event.objectId(),
+              event.refName(),
+              event.eventCreatedOn(),
+              event.isDelete(),
+              state);
       fetchCallsPool
-          .submit(() -> sources.get().getAll().parallelStream().forEach(callFunction))
+          .submit(() -> allSources.parallelStream().forEach(callFunction))
           .get(fetchCallsTimeout, TimeUnit.MILLISECONDS);
     } catch (InterruptedException | ExecutionException | TimeoutException e) {
       stateLog.error(
@@ -539,12 +534,7 @@
       String eventKey = String.format("%s:%s", event.projectName(), event.refName());
       if (!eventsReplayed.contains(eventKey)) {
         repLog.info("Firing pending task {}", event);
-        fire(
-            event.projectName(),
-            event.objectId(),
-            event.refName(),
-            event.eventCreatedOn(),
-            event.isDelete());
+        fire(event);
         eventsReplayed.add(eventKey);
       }
     }
@@ -578,6 +568,15 @@
           projectName, refName, objectId, eventCreatedOn, isDelete);
     }
 
+    static ReferenceUpdatedEvent from(RefUpdatedEvent event) {
+      return ReferenceUpdatedEvent.create(
+          event.refUpdate.get().project,
+          event.getRefName(),
+          ObjectId.fromString(event.refUpdate.get().newRev),
+          event.eventCreatedOn,
+          ZEROS_OBJECTID.equals(event.refUpdate.get().newRev));
+    }
+
     public abstract String projectName();
 
     public abstract String refName();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java
index a8799c2..d7ae063 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParser.java
@@ -17,6 +17,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.config.GerritIsReplica;
+import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.replication.ConfigParser;
 import com.googlesource.gerrit.plugins.replication.RemoteConfiguration;
 import java.net.URISyntaxException;
@@ -32,6 +34,13 @@
 
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private boolean isReplica;
+
+  @Inject
+  SourceConfigParser(@GerritIsReplica Boolean isReplica) {
+    this.isReplica = isReplica;
+  }
+
   /* (non-Javadoc)
    * @see com.googlesource.gerrit.plugins.replication.ConfigParser#parseRemotes(org.eclipse.jgit.lib.Config)
    */
@@ -45,7 +54,7 @@
 
     ImmutableList.Builder<RemoteConfiguration> sourceConfigs = ImmutableList.builder();
     for (RemoteConfig c : allFetchRemotes(config)) {
-      if (c.getURIs().isEmpty()) {
+      if (isReplica && c.getURIs().isEmpty()) {
         continue;
       }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
index 72f0266..d535ac9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectAction.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
@@ -33,6 +34,7 @@
 import java.util.Objects;
 import javax.servlet.http.HttpServletResponse;
 
+@Singleton
 public class ApplyObjectAction implements RestModifyView<ProjectResource, RevisionInput> {
 
   private final ApplyObjectCommand applyObjectCommand;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
index f897012..e49c8b6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
@@ -35,7 +35,6 @@
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
 import com.googlesource.gerrit.plugins.replication.pull.Source;
 import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection;
-import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject;
 import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
 import java.io.IOException;
 import java.util.Optional;
@@ -49,7 +48,6 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final PullReplicationStateLogger fetchStateLog;
-  private final ApplyObject applyObject;
   private final DynamicItem<EventDispatcher> eventDispatcher;
   private final ProjectCache projectCache;
   private final SourcesCollection sourcesCollection;
@@ -61,13 +59,11 @@
       PullReplicationStateLogger fetchStateLog,
       ProjectCache projectCache,
       SourcesCollection sourcesCollection,
-      ApplyObject applyObject,
       PermissionBackend permissionBackend,
       DynamicItem<EventDispatcher> eventDispatcher,
       LocalGitRepositoryManagerProvider gitManagerProvider) {
     this.fetchStateLog = fetchStateLog;
     this.projectCache = projectCache;
-    this.applyObject = applyObject;
     this.eventDispatcher = eventDispatcher;
     this.sourcesCollection = sourcesCollection;
     this.permissionBackend = permissionBackend;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
index 04797bd..9e69f8d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.ioutil.HexFormat;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob.Factory;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.RemoteConfigurationMissingException;
@@ -38,6 +39,7 @@
 import java.util.concurrent.TimeoutException;
 import org.eclipse.jgit.errors.TransportException;
 
+@Singleton
 public class FetchAction implements RestModifyView<ProjectResource, Input> {
   private final FetchCommand command;
   private final WorkQueue workQueue;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
index 73a4ac5..27afcfd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchApiCapability.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 
 public class FetchApiCapability extends CapabilityDefinition {
-  static final String CALL_FETCH_ACTION = "callFetchAction";
+  public static final String CALL_FETCH_ACTION = "callFetchAction";
 
   @Override
   public String getDescription() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
index b8249ae..5323425 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
@@ -97,7 +97,12 @@
       if (fetchType == ReplicationType.ASYNC) {
         state.markAllFetchTasksScheduled();
         Future<?> future = source.get().schedule(name, refName, state, apiRequestMetrics);
-        future.get(source.get().getTimeout(), TimeUnit.SECONDS);
+        int timeout = source.get().getTimeout();
+        if (timeout == 0) {
+          future.get();
+        } else {
+          future.get(timeout, TimeUnit.SECONDS);
+        }
       } else {
         Optional<FetchOne> maybeFetch =
             source
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
index 77d0e0b..7ca8805 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
@@ -26,8 +26,10 @@
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException;
 
+@Singleton
 public class FetchPreconditions {
   private final String pluginName;
   private final PermissionBackend permissionBackend;
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
index 0f3e1e8..95082b8 100644
--- 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
@@ -16,7 +16,6 @@
 
 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.name.Names;
@@ -24,12 +23,10 @@
 import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
 
 public class HttpModule extends ServletModule {
-  private boolean isReplica;
   private final BearerTokenProvider bearerTokenProvider;
 
   @Inject
-  public HttpModule(@GerritIsReplica Boolean isReplica, BearerTokenProvider bearerTokenProvider) {
-    this.isReplica = isReplica;
+  public HttpModule(BearerTokenProvider bearerTokenProvider) {
     this.bearerTokenProvider = bearerTokenProvider;
   }
 
@@ -49,12 +46,8 @@
                   .in(Scopes.SINGLETON);
             });
 
-    if (isReplica) {
-      DynamicSet.bind(binder(), AllRequestFilter.class)
-          .to(PullReplicationFilter.class)
-          .in(Scopes.SINGLETON);
-    } else {
-      serveRegex("/init-project/.*$").with(ProjectInitializationAction.class);
-    }
+    DynamicSet.bind(binder(), AllRequestFilter.class)
+        .to(PullReplicationFilter.class)
+        .in(Scopes.SINGLETON);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
index 2e1c5d4..adb333c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
@@ -27,11 +27,13 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.LocalFS;
 import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps;
 import java.util.Optional;
 import org.eclipse.jgit.transport.URIish;
 
+@Singleton
 class ProjectDeletionAction
     implements RestModifyView<ProjectResource, ProjectDeletionAction.DeleteInput> {
   private static final PluginPermission DELETE_PROJECT =
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
deleted file mode 100644
index d1d28a6..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2020 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.server.project.ProjectResource.PROJECT_KIND;
-import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION;
-
-import com.google.gerrit.extensions.annotations.Exports;
-import com.google.gerrit.extensions.config.CapabilityDefinition;
-import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.inject.Scopes;
-
-public class PullReplicationApiModule extends RestApiModule {
-  @Override
-  protected void configure() {
-    bind(FetchAction.class).in(Scopes.SINGLETON);
-    bind(ApplyObjectAction.class).in(Scopes.SINGLETON);
-    bind(ProjectDeletionAction.class).in(Scopes.SINGLETON);
-    bind(UpdateHeadAction.class).in(Scopes.SINGLETON);
-    post(PROJECT_KIND, "fetch").to(FetchAction.class);
-    post(PROJECT_KIND, "apply-object").to(ApplyObjectAction.class);
-    post(PROJECT_KIND, "apply-objects").to(ApplyObjectsAction.class);
-    delete(PROJECT_KIND, "delete-project").to(ProjectDeletionAction.class);
-    put(PROJECT_KIND, "HEAD").to(UpdateHeadAction.class);
-
-    bind(FetchPreconditions.class).in(Scopes.SINGLETON);
-    bind(CapabilityDefinition.class)
-        .annotatedWith(Exports.named(CALL_FETCH_ACTION))
-        .to(FetchApiCapability.class);
-  }
-}
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
index e54d408..5a4393c 100644
--- 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
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY;
 import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.checkAcceptHeader;
-import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.setResponse;
 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;
@@ -26,6 +25,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.api.projects.HeadInput;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -35,24 +35,26 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.ProjectsCollection;
+import com.google.gerrit.server.project.ProjectState;
 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 com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
-import com.googlesource.gerrit.plugins.replication.pull.api.exception.InitProjectException;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException;
 import java.io.BufferedReader;
 import java.io.EOFException;
@@ -83,9 +85,10 @@
   private ProjectInitializationAction projectInitializationAction;
   private UpdateHeadAction updateHEADAction;
   private ProjectDeletionAction projectDeletionAction;
-  private ProjectsCollection projectsCollection;
+  private ProjectCache projectCache;
   private Gson gson;
   private String pluginName;
+  private final Provider<CurrentUser> currentUserProvider;
 
   @Inject
   public PullReplicationFilter(
@@ -95,17 +98,19 @@
       ProjectInitializationAction projectInitializationAction,
       UpdateHeadAction updateHEADAction,
       ProjectDeletionAction projectDeletionAction,
-      ProjectsCollection projectsCollection,
-      @PluginName String pluginName) {
+      ProjectCache projectCache,
+      @PluginName String pluginName,
+      Provider<CurrentUser> currentUserProvider) {
     this.fetchAction = fetchAction;
     this.applyObjectAction = applyObjectAction;
     this.applyObjectsAction = applyObjectsAction;
     this.projectInitializationAction = projectInitializationAction;
     this.updateHEADAction = updateHEADAction;
     this.projectDeletionAction = projectDeletionAction;
-    this.projectsCollection = projectsCollection;
+    this.projectCache = projectCache;
     this.pluginName = pluginName;
     this.gson = OutputFormat.JSON.newGsonBuilder().create();
+    this.currentUserProvider = currentUserProvider;
   }
 
   @Override
@@ -120,19 +125,25 @@
     HttpServletRequest httpRequest = (HttpServletRequest) request;
     try {
       if (isFetchAction(httpRequest)) {
+        failIfcurrentUserIsAnonymous();
         writeResponse(httpResponse, doFetch(httpRequest));
       } else if (isApplyObjectAction(httpRequest)) {
+        failIfcurrentUserIsAnonymous();
         writeResponse(httpResponse, doApplyObject(httpRequest));
       } else if (isApplyObjectsAction(httpRequest)) {
+        failIfcurrentUserIsAnonymous();
         writeResponse(httpResponse, doApplyObjects(httpRequest));
       } else if (isInitProjectAction(httpRequest)) {
+        failIfcurrentUserIsAnonymous();
         if (!checkAcceptHeader(httpRequest, httpResponse)) {
           return;
         }
         doInitProject(httpRequest, httpResponse);
       } else if (isUpdateHEADAction(httpRequest)) {
+        failIfcurrentUserIsAnonymous();
         writeResponse(httpResponse, doUpdateHEAD(httpRequest));
       } else if (isDeleteProjectAction(httpRequest)) {
+        failIfcurrentUserIsAnonymous();
         writeResponse(httpResponse, doDeleteProject(httpRequest));
       } else {
         chain.doFilter(request, response);
@@ -156,7 +167,7 @@
     } catch (ResourceConflictException e) {
       RestApiServlet.replyError(
           httpRequest, httpResponse, SC_CONFLICT, e.getMessage(), e.caching(), e);
-    } catch (InitProjectException | ResourceNotFoundException e) {
+    } catch (ResourceNotFoundException e) {
       RestApiServlet.replyError(
           httpRequest, httpResponse, SC_INTERNAL_SERVER_ERROR, e.getMessage(), e.caching(), e);
     } catch (NoSuchElementException e) {
@@ -173,17 +184,16 @@
     }
   }
 
-  private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
-      throws RestApiException, IOException, PermissionBackendException {
-
-    IdString id = getInitProjectName(httpRequest).get();
-    String projectName = id.get();
-    if (projectInitializationAction.initProject(projectName)) {
-      setResponse(
-          httpResponse, HttpServletResponse.SC_CREATED, "Project " + projectName + " initialized");
-      return;
+  private void failIfcurrentUserIsAnonymous() throws UnauthorizedAuthException {
+    CurrentUser currentUser = currentUserProvider.get();
+    if (currentUser instanceof AnonymousUser) {
+      throw new UnauthorizedAuthException();
     }
-    throw new InitProjectException(projectName);
+  }
+
+  private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
+      throws IOException, ServletException {
+    projectInitializationAction.service(httpRequest, httpResponse);
   }
 
   @SuppressWarnings("unchecked")
@@ -191,9 +201,8 @@
       throws RestApiException, IOException, PermissionBackendException {
     RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class));
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
 
-    return (Response<String>) applyObjectAction.apply(projectResource, input);
+    return (Response<String>) applyObjectAction.apply(parseProjectResource(id), input);
   }
 
   @SuppressWarnings("unchecked")
@@ -201,26 +210,24 @@
       throws RestApiException, IOException, PermissionBackendException {
     RevisionsInput input = readJson(httpRequest, TypeLiteral.get(RevisionsInput.class));
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
 
-    return (Response<String>) applyObjectsAction.apply(projectResource, input);
+    return (Response<String>) applyObjectsAction.apply(parseProjectResource(id), input);
   }
 
   @SuppressWarnings("unchecked")
   private Response<String> doUpdateHEAD(HttpServletRequest httpRequest) throws Exception {
     HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class));
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
 
-    return (Response<String>) updateHEADAction.apply(projectResource, input);
+    return (Response<String>) updateHEADAction.apply(parseProjectResource(id), input);
   }
 
   @SuppressWarnings("unchecked")
   private Response<String> doDeleteProject(HttpServletRequest httpRequest) throws Exception {
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
     return (Response<String>)
-        projectDeletionAction.apply(projectResource, new ProjectDeletionAction.DeleteInput());
+        projectDeletionAction.apply(
+            parseProjectResource(id), new ProjectDeletionAction.DeleteInput());
   }
 
   @SuppressWarnings("unchecked")
@@ -228,9 +235,16 @@
       throws IOException, RestApiException, PermissionBackendException {
     Input input = readJson(httpRequest, TypeLiteral.get(Input.class));
     IdString id = getProjectName(httpRequest).get();
-    ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id);
 
-    return (Response<Map<String, Object>>) fetchAction.apply(projectResource, input);
+    return (Response<Map<String, Object>>) fetchAction.apply(parseProjectResource(id), input);
+  }
+
+  private ProjectResource parseProjectResource(IdString id) throws ResourceNotFoundException {
+    Optional<ProjectState> project = projectCache.get(Project.nameKey(id.get()));
+    if (project.isEmpty()) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new ProjectResource(project.get(), currentUserProvider.get());
   }
 
   private <T> void writeResponse(HttpServletResponse httpResponse, Response<T> response)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java
deleted file mode 100644
index 85a7729..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// 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.exception;
-
-import com.google.gerrit.extensions.restapi.RestApiException;
-
-public class InitProjectException extends RestApiException {
-  private static final long serialVersionUID = 1L;
-
-  public InitProjectException(String projectName) {
-    super("Cannot create project " + projectName);
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
index 24898e6..d3e45da 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
@@ -28,6 +28,8 @@
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
@@ -101,7 +103,9 @@
     if (credentialsProvider.supports(user, pass)
         && credentialsProvider.get(uri, user, pass)
         && uri.getScheme() != null
-        && !"ssh".equalsIgnoreCase(uri.getScheme())) {
+        && !"ssh".equalsIgnoreCase(uri.getScheme())
+        && StringUtils.isNotEmpty(user.getValue())
+        && ArrayUtils.isNotEmpty(pass.getValue())) {
       return uri.setUser(user.getValue()).setPass(String.valueOf(pass.getValue()));
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java
index 9a10898..434c0f0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetchValidator.java
@@ -35,7 +35,7 @@
 
   @Override
   public Void visit(AssistedInjectBinding<? extends FetchFactory> binding) {
-    TypeLiteral<CGitFetch> nativeGitFetchType = new TypeLiteral<CGitFetch>() {};
+    TypeLiteral<CGitFetch> nativeGitFetchType = new TypeLiteral<>() {};
     for (AssistedMethod method : binding.getAssistedMethods()) {
       if (method.getImplementationType().equals(nativeGitFetchType)) {
         String[] command = new String[] {"git", "--version"};
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java
index acb68cf..0fa89b5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/PermanentTransportException.java
@@ -14,7 +14,7 @@
 
 package com.googlesource.gerrit.plugins.replication.pull.fetch;
 
-import com.jcraft.jsch.JSchException;
+import org.apache.sshd.common.SshException;
 import org.eclipse.jgit.errors.TransportException;
 
 public class PermanentTransportException extends TransportException {
@@ -26,7 +26,8 @@
 
   public static TransportException wrapIfPermanentTransportException(TransportException e) {
     Throwable cause = e.getCause();
-    if (cause instanceof JSchException && cause.getMessage().startsWith("UnknownHostKey:")) {
+    if (cause instanceof SshException
+        && cause.getMessage().startsWith("Failed (UnsupportedCredentialItem) to execute:")) {
       return new PermanentTransportException("Terminal fetch failure", e);
     }
 
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 73c5c05..22b52e7 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -16,10 +16,6 @@
 group that is granted the 'Pull Replication' capability (provided
 by this plugin) or the 'Administrate Server' capability.
 
-When replicating hidden projects, the pull replication user needs to have
-the 'Administrate Server' capability or being added as the owner of each
-individual project that is supposed to be replicated.
-
 Change Indexing
 --------
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
index ee286bb..dd9f98e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
@@ -18,9 +18,9 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.*;
 
+import com.google.gerrit.acceptance.TestMetricMaker;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.PerThreadRequestScope;
 import com.google.gerrit.server.util.IdGenerator;
@@ -54,6 +54,7 @@
   private final Project.NameKey PROJECT_NAME = Project.NameKey.parse(TEST_PROJECT_NAME);
   private final String TEST_REF = "refs/heads/refForReplicationTask";
   private final String URI_PATTERN = "http://test.com/" + TEST_PROJECT_NAME + ".git";
+  private final TestMetricMaker testMetricMaker = new TestMetricMaker();
 
   @Mock private GitRepositoryManager grm;
   @Mock private Repository repository;
@@ -72,8 +73,9 @@
 
   @Before
   public void setup() throws Exception {
+    testMetricMaker.reset();
     FetchReplicationMetrics fetchReplicationMetrics =
-        new FetchReplicationMetrics("pull-replication", new DisabledMetricMaker());
+        new FetchReplicationMetrics("pull-replication", testMetricMaker);
     urIish = new URIish(URI_PATTERN);
 
     grm = mock(GitRepositoryManager.class);
@@ -704,6 +706,51 @@
             TEST_PROJECT_NAME, TEST_REF, urIish, ReplicationState.RefFetchResult.FAILED, null);
   }
 
+  @Test
+  public void shouldNotRecordReplicationLatencyMetricIfAllRefsAreExcluded() throws Exception {
+    setupMocks(true);
+    String filteredRef = "refs/heads/filteredRef";
+    Set<String> refSpecs = Set.of(TEST_REF, filteredRef);
+    createTestStates(TEST_REF, 1);
+    createTestStates(filteredRef, 1);
+    setupFetchFactoryMock(
+        List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()),
+        Optional.of(List.of(TEST_REF)));
+    objectUnderTest.addRefs(refSpecs);
+    objectUnderTest.setReplicationFetchFilter(replicationFilter);
+    ReplicationFetchFilter mockFilter = mock(ReplicationFetchFilter.class);
+    when(replicationFilter.get()).thenReturn(mockFilter);
+    when(mockFilter.filter(TEST_PROJECT_NAME, refSpecs)).thenReturn(Collections.emptySet());
+
+    objectUnderTest.run();
+
+    verify(pullReplicationApiRequestMetrics, never()).stop(any());
+    assertThat(testMetricMaker.getTimer("replication_latency")).isEqualTo(0);
+  }
+
+  @Test
+  public void shouldRecordReplicationLatencyMetricWhenAtLeastOneRefWasReplicated()
+      throws Exception {
+    setupMocks(true);
+    String filteredRef = "refs/heads/filteredRef";
+    Set<String> refSpecs = Set.of(TEST_REF, filteredRef);
+    createTestStates(TEST_REF, 1);
+    createTestStates(filteredRef, 1);
+    setupFetchFactoryMock(
+        List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()),
+        Optional.of(List.of(TEST_REF)));
+    objectUnderTest.addRefs(refSpecs);
+    objectUnderTest.setReplicationFetchFilter(replicationFilter);
+    ReplicationFetchFilter mockFilter = mock(ReplicationFetchFilter.class);
+    when(replicationFilter.get()).thenReturn(mockFilter);
+    when(mockFilter.filter(TEST_PROJECT_NAME, refSpecs)).thenReturn(Set.of(TEST_REF));
+
+    objectUnderTest.run();
+
+    verify(pullReplicationApiRequestMetrics).stop(any());
+    assertThat(testMetricMaker.getTimer("replication_latency")).isGreaterThan(0);
+  }
+
   private void setupRequestScopeMock() {
     when(scoper.scope(any()))
         .thenAnswer(
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java
index 09a465c..fcb0702 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PermanentFailureExceptionTest.java
@@ -18,7 +18,7 @@
 
 import com.googlesource.gerrit.plugins.replication.pull.fetch.InexistentRefTransportException;
 import com.googlesource.gerrit.plugins.replication.pull.fetch.PermanentTransportException;
-import com.jcraft.jsch.JSchException;
+import org.apache.sshd.common.SshException;
 import org.eclipse.jgit.errors.TransportException;
 import org.junit.Test;
 
@@ -29,7 +29,9 @@
     assertThat(
             PermanentTransportException.wrapIfPermanentTransportException(
                 new TransportException(
-                    "SSH error", new JSchException("UnknownHostKey: some.place"))))
+                    "SSH error",
+                    new SshException(
+                        "Failed (UnsupportedCredentialItem) to execute: some.commands"))))
         .isInstanceOf(PermanentTransportException.class);
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
index 5cf01b1..0721dd2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
@@ -49,7 +49,8 @@
 @UseLocalDisk
 @TestPlugin(
     name = "pull-replication",
-    sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule")
+    sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
+    httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
 public class PullReplicationFanoutConfigIT extends LightweightPluginDaemonTest {
   private static final Optional<String> ALL_PROJECTS = Optional.empty();
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
index 18f7a0c..29bf7e4 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 The Android Open Source Project
+// Copyright (C) 2020 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.
@@ -14,9 +14,45 @@
 
 package com.googlesource.gerrit.plugins.replication.pull;
 
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.acceptance.GitUtil.pushOne;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+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.config.GerritConfig;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.events.HeadUpdatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
+import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.eclipse.jgit.transport.URIish;
+import org.junit.Ignore;
+import org.junit.Test;
 
 @SkipProjectClone
 @UseLocalDisk
@@ -25,4 +61,346 @@
     sysModule =
         "com.googlesource.gerrit.plugins.replication.pull.PullReplicationITAbstract$PullReplicationTestModule",
     httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
-public class PullReplicationIT extends PullReplicationITAbstract {}
+public class PullReplicationIT extends PullReplicationSetupBase {
+
+  @Override
+  protected void setReplicationSource(
+      String remoteName, List<String> replicaSuffixes, Optional<String> project)
+      throws IOException {
+    List<String> fetchUrls =
+        buildReplicaURLs(replicaSuffixes, s -> gitPath.resolve("${name}" + s + ".git").toString());
+    config.setStringList("remote", remoteName, "url", fetchUrls);
+    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
+    config.setString("remote", remoteName, "fetch", "+refs/*:refs/*");
+    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();
+  }
+
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    setUpTestPlugin(false);
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateNewChangeRef() throws Exception {
+    testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+
+    Result pushResult = createChange();
+    RevCommit sourceCommit = pushResult.getCommit();
+    String sourceRef = pushResult.getPatchSet().refName();
+
+    ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            sourceRef,
+            ObjectId.zeroId().getName(),
+            sourceCommit.getId().getName(),
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
+
+      Ref targetBranchRef = getRef(repo, sourceRef);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateNewBranch() throws Exception {
+    String testProjectName = project + TEST_REPLICATION_SUFFIX;
+    createTestProject(testProjectName);
+
+    String newBranch = "refs/heads/mybranch";
+    String master = "refs/heads/master";
+    BranchInput input = new BranchInput();
+    input.revision = master;
+    gApi.projects().name(testProjectName).branch(newBranch).create(input);
+    String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision;
+
+    ReplicationQueue pullReplicationQueue =
+        plugin.getSysInjector().getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            ObjectId.zeroId().getName(),
+            branchRevision,
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project);
+        Repository sourceRepo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, newBranch) != null);
+
+      Ref targetBranchRef = getRef(repo, newBranch);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId().getName()).isEqualTo(branchRevision);
+    }
+  }
+
+  @Test
+  @UseLocalDisk
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateForceUpdatedBranch() throws Exception {
+    boolean forcedPush = true;
+    String testProjectName = project + TEST_REPLICATION_SUFFIX;
+    NameKey testProjectNameKey = createTestProject(testProjectName);
+
+    String newBranch = "refs/heads/mybranch";
+    String master = "refs/heads/master";
+    BranchInput input = new BranchInput();
+    input.revision = master;
+    gApi.projects().name(testProjectName).branch(newBranch).create(input);
+
+    projectOperations
+        .project(testProjectNameKey)
+        .forUpdate()
+        .add(allow(Permission.PUSH).ref(newBranch).group(REGISTERED_USERS).force(true))
+        .update();
+
+    String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision;
+
+    ReplicationQueue pullReplicationQueue =
+        plugin.getSysInjector().getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            ObjectId.zeroId().getName(),
+            branchRevision,
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, newBranch) != null);
+
+      Ref targetBranchRef = getRef(repo, newBranch);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId().getName()).isEqualTo(branchRevision);
+    }
+
+    TestRepository<InMemoryRepository> testProject = cloneProject(testProjectNameKey);
+    fetch(testProject, RefNames.REFS_HEADS + "*:" + RefNames.REFS_HEADS + "*");
+    RevCommit amendedCommit = testProject.amendRef(newBranch).message("Amended commit").create();
+    PushResult pushResult =
+        pushOne(testProject, newBranch, newBranch, false, forcedPush, Collections.emptyList());
+    Collection<RemoteRefUpdate> pushedRefs = pushResult.getRemoteUpdates();
+    assertThat(pushedRefs).hasSize(1);
+    assertThat(pushedRefs.iterator().next().getStatus()).isEqualTo(Status.OK);
+
+    FakeGitReferenceUpdatedEvent forcedPushEvent =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            branchRevision,
+            amendedCommit.getId().getName(),
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(forcedPushEvent);
+
+    try (Repository repo = repoManager.openRepository(project);
+        Repository sourceRepo = repoManager.openRepository(project)) {
+      waitUntil(
+          () ->
+              checkedGetRef(repo, newBranch) != null
+                  && checkedGetRef(repo, newBranch)
+                      .getObjectId()
+                      .getName()
+                      .equals(amendedCommit.getId().getName()));
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateNewChangeRefCGitClient() throws Exception {
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+
+    config.setBoolean("replication", null, "useCGitClient", true);
+    config.save();
+
+    autoReloadConfigDecorator.reload();
+
+    testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+
+    Result pushResult = createChange();
+    RevCommit sourceCommit = pushResult.getCommit();
+    String sourceRef = pushResult.getPatchSet().refName();
+
+    ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            sourceRef,
+            ObjectId.zeroId().getName(),
+            sourceCommit.getId().getName(),
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
+
+      Ref targetBranchRef = getRef(repo, sourceRef);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateNewBranchCGitClient() throws Exception {
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+
+    config.setBoolean("replication", null, "useCGitClient", true);
+    config.save();
+
+    autoReloadConfigDecorator.reload();
+
+    String testProjectName = project + TEST_REPLICATION_SUFFIX;
+    createTestProject(testProjectName);
+
+    String newBranch = "refs/heads/mybranch";
+    String master = "refs/heads/master";
+    BranchInput input = new BranchInput();
+    input.revision = master;
+    gApi.projects().name(testProjectName).branch(newBranch).create(input);
+    String branchRevision = gApi.projects().name(testProjectName).branch(newBranch).get().revision;
+
+    ReplicationQueue pullReplicationQueue =
+        plugin.getSysInjector().getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            newBranch,
+            ObjectId.zeroId().getName(),
+            branchRevision,
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project);
+        Repository sourceRepo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, newBranch) != null);
+
+      Ref targetBranchRef = getRef(repo, newBranch);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId().getName()).isEqualTo(branchRevision);
+    }
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldCreateNewProject() throws Exception {
+    NameKey projectToCreate = Project.nameKey(project.get() + "_created");
+
+    setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(projectToCreate.get()));
+    config.save();
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+    autoReloadConfigDecorator.reload();
+    Source source =
+        getInstance(SourcesCollection.class).getByRemoteName(TEST_REPLICATION_REMOTE).get();
+
+    FetchApiClient client = getInstance(FetchApiClient.Factory.class).create(source);
+    client.initProject(projectToCreate, new URIish(source.getApis().get(0)));
+
+    waitUntil(() -> repoManager.list().contains(projectToCreate));
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateProjectDeletion() throws Exception {
+    String projectToDelete = project.get();
+    setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(projectToDelete));
+    config.save();
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+    autoReloadConfigDecorator.reload();
+
+    ProjectDeletedListener.Event event =
+        new ProjectDeletedListener.Event() {
+          @Override
+          public String getProjectName() {
+            return projectToDelete;
+          }
+
+          @Override
+          public NotifyHandling getNotify() {
+            return NotifyHandling.NONE;
+          }
+        };
+    for (ProjectDeletedListener l : deletedListeners) {
+      l.onProjectDeleted(event);
+    }
+
+    waitUntil(() -> !repoManager.list().contains(project));
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateHeadUpdate() throws Exception {
+    String testProjectName = project.get();
+    setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(testProjectName));
+    config.save();
+    AutoReloadConfigDecorator autoReloadConfigDecorator =
+        getInstance(AutoReloadConfigDecorator.class);
+    autoReloadConfigDecorator.reload();
+
+    String newBranch = "refs/heads/mybranch";
+    String master = "refs/heads/master";
+    BranchInput input = new BranchInput();
+    input.revision = master;
+    gApi.projects().name(testProjectName).branch(newBranch).create(input);
+
+    ReplicationQueue pullReplicationQueue =
+        plugin.getSysInjector().getInstance(ReplicationQueue.class);
+
+    HeadUpdatedListener.Event event = new FakeHeadUpdateEvent(master, newBranch, testProjectName);
+    pullReplicationQueue.onHeadUpdated(event);
+
+    waitUntil(
+        () -> {
+          try {
+            return gApi.projects().name(testProjectName).head().equals(newBranch);
+          } catch (RestApiException e) {
+            return false;
+          }
+        });
+  }
+
+  @Ignore
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReplicateNewChangeRefToReplica() throws Exception {
+    testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+
+    Result pushResult = createChange();
+    RevCommit sourceCommit = pushResult.getCommit();
+    String sourceRef = pushResult.getPatchSet().refName();
+
+    ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
+    FakeGitReferenceUpdatedEvent event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            sourceRef,
+            ObjectId.zeroId().getName(),
+            sourceCommit.getId().getName(),
+            TEST_REPLICATION_REMOTE);
+    pullReplicationQueue.onEvent(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
+
+      Ref targetBranchRef = getRef(repo, sourceRef);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
+    }
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
index 1cffec7..603528d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
@@ -369,7 +369,7 @@
   }
 
   @Test
-  public void shouldCallDeleteWhenReplicateProjectDeletionsTrue() throws Exception {
+  public void shouldCallDeleteWhenReplicateProjectDeletionsTrue() {
     when(source.wouldDeleteProject(any())).thenReturn(true);
 
     String projectName = "testProject";
@@ -385,7 +385,7 @@
   }
 
   @Test
-  public void shouldNotCallDeleteWhenProjectNotToDelete() throws Exception {
+  public void shouldNotCallDeleteWhenProjectNotToDelete() {
     when(source.wouldDeleteProject(any())).thenReturn(false);
 
     FakeProjectDeletedEvent event = new FakeProjectDeletedEvent("testProject");
@@ -397,7 +397,7 @@
   }
 
   @Test
-  public void shouldScheduleUpdateHeadWhenWouldFetchProject() throws Exception {
+  public void shouldScheduleUpdateHeadWhenWouldFetchProject() {
     when(source.wouldFetchProject(any())).thenReturn(true);
 
     String projectName = "aProject";
@@ -413,7 +413,7 @@
   }
 
   @Test
-  public void shouldNotScheduleUpdateHeadWhenNotWouldFetchProject() throws Exception {
+  public void shouldNotScheduleUpdateHeadWhenNotWouldFetchProject() {
     when(source.wouldFetchProject(any())).thenReturn(false);
 
     String projectName = "aProject";
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
index a46846e..4a3fa55 100644
--- 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
@@ -162,7 +162,7 @@
   }
 
   public ResponseHandler<Object> assertHttpResponseCode(int responseCode) {
-    return new ResponseHandler<Object>() {
+    return new ResponseHandler<>() {
 
       @Override
       public Object handleResponse(HttpResponse response)
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
index f1f9e44..fc1b02c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
@@ -102,7 +102,6 @@
             fetchStateLog,
             projectCache,
             sourceCollection,
-            applyObject,
             permissionBackend,
             eventDispatcherDataItem,
             new LocalGitRepositoryManagerProvider(gitManager));
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
index 10415e4..cf7515e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
@@ -94,10 +94,11 @@
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
-  public void shouldReturnForbiddenForUserWithoutPermissionsOnReplica() throws Exception {
+  public void shouldReturnUnauthorizedForUserWithoutPermissionsOnReplica() throws Exception {
     httpClientFactory
         .create(source)
-        .execute(createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN));
+        .execute(
+            createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED));
   }
 
   @Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
index c543969..0f13881 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
@@ -184,12 +184,13 @@
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
-  public void shouldReturnForbiddenForUserWithoutPermissionsWhenNodeIsAReplica() throws Exception {
+  public void shouldReturnUnauthorizedForUserWithoutPermissionsWhenNodeIsAReplica()
+      throws Exception {
     httpClientFactory
         .create(source)
         .execute(
             createPutRequestWithHeaders(),
-            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN));
+            assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED));
   }
 
   @Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
index 9e5ca8d..9492d49 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
@@ -4,18 +4,25 @@
 import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY;
 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.mockito.internal.verification.VerificationModeFactory.atLeastOnce;
 import static org.mockito.internal.verification.VerificationModeFactory.times;
 
 import com.google.common.net.MediaType;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.*;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.ProjectsCollection;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.util.Providers;
 import java.io.*;
 import java.nio.charset.StandardCharsets;
+import java.util.Optional;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletRequest;
@@ -37,10 +44,12 @@
   @Mock private ProjectInitializationAction projectInitializationAction;
   @Mock private UpdateHeadAction updateHEADAction;
   @Mock private ProjectDeletionAction projectDeletionAction;
-  @Mock private ProjectsCollection projectsCollection;
-  @Mock private ProjectResource projectResource;
+  @Mock private ProjectCache projectCache;
+  @Mock private ProjectState projectState;
   @Mock private ServletOutputStream outputStream;
   @Mock private PrintWriter printWriter;
+  @Mock private IdentifiedUser identifiedUserMock;
+  @Mock private AnonymousUser anonymousUserMock;
   private final String PLUGIN_NAME = "pull-replication";
   private final String PROJECT_NAME = "some-project";
   private final String PROJECT_NAME_GIT = "some-project.git";
@@ -60,6 +69,10 @@
   private final Response OK_RESPONSE = Response.ok();
 
   private PullReplicationFilter createPullReplicationFilter() {
+    return createPullReplicationFilter(identifiedUserMock);
+  }
+
+  private PullReplicationFilter createPullReplicationFilter(CurrentUser currentUser) {
     return new PullReplicationFilter(
         fetchAction,
         applyObjectAction,
@@ -67,8 +80,9 @@
         projectInitializationAction,
         updateHEADAction,
         projectDeletionAction,
-        projectsCollection,
-        PLUGIN_NAME);
+        projectCache,
+        PLUGIN_NAME,
+        Providers.of(currentUser));
   }
 
   private void defineBehaviours(byte[] payload, String uri) throws Exception {
@@ -76,15 +90,14 @@
     InputStream is = new ByteArrayInputStream(payload);
     BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
     when(request.getReader()).thenReturn(bufferedReader);
-    when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)))
-        .thenReturn(projectResource);
+    when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState));
     when(response.getWriter()).thenReturn(printWriter);
   }
 
   private void verifyBehaviours() throws Exception {
     verify(request, atLeastOnce()).getRequestURI();
     verify(request).getReader();
-    verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME));
+    verify(projectCache).get(Project.nameKey(PROJECT_NAME));
     verify(response).getWriter();
     verify(response).setContentType("application/json");
     verify(response).setStatus(HttpServletResponse.SC_OK);
@@ -107,7 +120,7 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verifyBehaviours();
-    verify(fetchAction).apply(eq(projectResource), any());
+    verify(fetchAction).apply(any(ProjectResource.class), any());
   }
 
   @Test
@@ -130,7 +143,7 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verifyBehaviours();
-    verify(applyObjectAction).apply(eq(projectResource), any());
+    verify(applyObjectAction).apply(any(ProjectResource.class), any());
   }
 
   @Test
@@ -152,7 +165,7 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verifyBehaviours();
-    verify(applyObjectsAction).apply(eq(projectResource), any());
+    verify(applyObjectsAction).apply(any(ProjectResource.class), any());
   }
 
   @Test
@@ -160,15 +173,11 @@
 
     when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI);
     when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString());
-    when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(true);
-    when(response.getWriter()).thenReturn(printWriter);
 
     final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter();
     pullReplicationFilter.doFilter(request, response, filterChain);
 
-    verify(request, times(5)).getRequestURI();
-    verify(projectInitializationAction).initProject(eq(PROJECT_NAME_GIT));
-    verify(response).getWriter();
+    verify(projectInitializationAction).service(request, response);
   }
 
   @Test
@@ -183,15 +192,14 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verifyBehaviours();
-    verify(updateHEADAction).apply(eq(projectResource), any());
+    verify(updateHEADAction).apply(any(ProjectResource.class), any());
   }
 
   @Test
   public void shouldFilterProjectDeletionAction() throws Exception {
     when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI);
     when(request.getMethod()).thenReturn("DELETE");
-    when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)))
-        .thenReturn(projectResource);
+    when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState));
     when(projectDeletionAction.apply(any(), any())).thenReturn(OK_RESPONSE);
     when(response.getWriter()).thenReturn(printWriter);
 
@@ -199,8 +207,8 @@
     pullReplicationFilter.doFilter(request, response, filterChain);
 
     verify(request, times(7)).getRequestURI();
-    verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME));
-    verify(projectDeletionAction).apply(eq(projectResource), any());
+    verify(projectCache).get(Project.nameKey(PROJECT_NAME));
+    verify(projectDeletionAction).apply(any(ProjectResource.class), any());
     verify(response).getWriter();
     verify(response).setContentType("application/json");
     verify(response).setStatus(OK_RESPONSE.statusCode());
@@ -215,6 +223,17 @@
   }
 
   @Test
+  public void shouldGoNextInChainWhenAnonymousRequestUriDoesNotMatch() throws Exception {
+    when(request.getRequestURI()).thenReturn("any-url");
+    lenient().when(response.getOutputStream()).thenReturn(outputStream);
+
+    final PullReplicationFilter pullReplicationFilter =
+        createPullReplicationFilter(anonymousUserMock);
+    pullReplicationFilter.doFilter(request, response, filterChain);
+    verify(filterChain).doFilter(request, response);
+  }
+
+  @Test
   public void shouldBe404WhenJsonIsMalformed() throws Exception {
     byte[] payloadMalformedJson = "some-json-malformed".getBytes(StandardCharsets.UTF_8);
     InputStream is = new ByteArrayInputStream(payloadMalformedJson);
@@ -230,24 +249,10 @@
   }
 
   @Test
-  public void shouldBe500WhenProjectCannotBeInitiated() throws Exception {
-    when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI);
-    when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString());
-    when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(false);
-    when(response.getOutputStream()).thenReturn(outputStream);
-
-    final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter();
-    pullReplicationFilter.doFilter(request, response, filterChain);
-
-    verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-  }
-
-  @Test
   public void shouldBe500WhenResourceNotFound() throws Exception {
     when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI);
     when(request.getMethod()).thenReturn("DELETE");
-    when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)))
-        .thenReturn(projectResource);
+    when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState));
     when(projectDeletionAction.apply(any(), any()))
         .thenThrow(new ResourceNotFoundException("resource not found"));
     when(response.getOutputStream()).thenReturn(outputStream);
@@ -280,6 +285,19 @@
   }
 
   @Test
+  public void shouldBe401WhenUserIsAnonymous() throws Exception {
+    byte[] payloadFetchAction = "{}".getBytes(StandardCharsets.UTF_8);
+
+    defineBehaviours(payloadFetchAction, FETCH_URI);
+    when(response.getOutputStream()).thenReturn(outputStream);
+
+    PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(anonymousUserMock);
+    pullReplicationFilter.doFilter(request, response, filterChain);
+
+    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+  }
+
+  @Test
   public void shouldBe422WhenEntityCannotBeProcessed() throws Exception {
     byte[] payloadFetchAction =
         ("{"
@@ -304,8 +322,7 @@
   public void shouldBe409WhenThereIsResourceConflict() throws Exception {
     when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI);
     when(request.getMethod()).thenReturn("DELETE");
-    when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)))
-        .thenReturn(projectResource);
+    when(projectCache.get(Project.nameKey(PROJECT_NAME))).thenReturn(Optional.of(projectState));
 
     when(projectDeletionAction.apply(any(), any()))
         .thenThrow(new ResourceConflictException("Resource conflict"));