merge branch 'stable-3.4' into stable-3.5

* stable-3.4:
  Allow update head with Fetch replication global capability
  Revert "Reuse Gerrit code for head update"
  Fix pull-replication update head with Gerrit replica
  Ignore all broken tests on Gerrit replica
  Add indirect commons-lang3 dependency
  Do not provide response body when apply-object is successful

Change-Id: I55e22646f0db8ed2c375c9cfc113e4b6c4e5111e
diff --git a/BUILD b/BUILD
index 7646282..0983983 100644
--- a/BUILD
+++ b/BUILD
@@ -17,6 +17,7 @@
     deps = [
         "//lib/commons:io",
         "//plugins/replication",
+        "@commons-lang3//jar",
         "@events-broker//jar:neverlink",
     ],
 )
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 b3379f2..72f0266 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
@@ -102,7 +102,7 @@
           input.getRevisionData(),
           input.getLabel(),
           input.getEventCreatedOn());
-      return Response.created(input);
+      return Response.created();
     } catch (MissingParentObjectException e) {
       repLog.error(
           "Apply object API *FAILED* from {} for {}:{} - {}",
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectsAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectsAction.java
index dd155d4..817ea00 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectsAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectsAction.java
@@ -102,7 +102,7 @@
           input.getRevisionsData(),
           input.getLabel(),
           input.getEventCreatedOn());
-      return Response.created(input);
+      return Response.created();
     } catch (MissingParentObjectException e) {
       repLog.error(
           "Apply object API *FAILED* from {} for {}:{} - {}",
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 161bcf4..77d0e0b 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
@@ -16,13 +16,17 @@
 
 import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION;
 
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.api.access.PluginPermission;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException;
 
 public class FetchPreconditions {
   private final String pluginName;
@@ -39,11 +43,31 @@
     this.permissionBackend = permissionBackend;
   }
 
-  public Boolean canCallFetchApi() {
-    CurrentUser currentUser = userProvider.get();
-    PermissionBackend.WithUser userPermission = permissionBackend.user(currentUser);
+  public Boolean canCallFetchApi() throws UnauthorizedAuthException {
+    CurrentUser currentUser = currentUser();
+    return canCallFetchApi(currentUser, permissionBackend.user(currentUser));
+  }
+
+  private Boolean canCallFetchApi(
+      CurrentUser currentUser, PermissionBackend.WithUser userPermission) {
     return currentUser.isInternalUser()
         || userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER)
         || userPermission.testOrFalse(new PluginPermission(pluginName, CALL_FETCH_ACTION));
   }
+
+  public Boolean canCallUpdateHeadApi(Project.NameKey projectNameKey, String ref)
+      throws PermissionBackendException, UnauthorizedAuthException {
+    CurrentUser currentUser = currentUser();
+    PermissionBackend.WithUser userAcls = permissionBackend.user(currentUser);
+    return canCallFetchApi(currentUser, userAcls)
+        || userAcls.project(projectNameKey).ref(ref).test(RefPermission.SET_HEAD);
+  }
+
+  private CurrentUser currentUser() throws UnauthorizedAuthException {
+    CurrentUser currentUser = userProvider.get();
+    if (!currentUser.isIdentifiedUser() && !currentUser.isInternalUser()) {
+      throw new UnauthorizedAuthException();
+    }
+    return currentUser;
+  }
 }
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 4d932d4..e54d408 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
@@ -23,6 +23,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.annotations.PluginName;
@@ -52,6 +53,7 @@
 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;
 import java.io.IOException;
@@ -136,6 +138,9 @@
         chain.doFilter(request, response);
       }
 
+    } catch (UnauthorizedAuthException e) {
+      RestApiServlet.replyError(
+          httpRequest, httpResponse, SC_UNAUTHORIZED, e.getMessage(), e.caching(), e);
     } catch (AuthException e) {
       RestApiServlet.replyError(
           httpRequest, httpResponse, SC_FORBIDDEN, e.getMessage(), e.caching(), e);
@@ -182,23 +187,23 @@
   }
 
   @SuppressWarnings("unchecked")
-  private Response<Map<String, Object>> doApplyObject(HttpServletRequest httpRequest)
+  private Response<String> doApplyObject(HttpServletRequest httpRequest)
       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<Map<String, Object>>) applyObjectAction.apply(projectResource, input);
+    return (Response<String>) applyObjectAction.apply(projectResource, input);
   }
 
   @SuppressWarnings("unchecked")
-  private Response<Map<String, Object>> doApplyObjects(HttpServletRequest httpRequest)
+  private Response<String> doApplyObjects(HttpServletRequest httpRequest)
       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<Map<String, Object>>) applyObjectsAction.apply(projectResource, input);
+    return (Response<String>) applyObjectsAction.apply(projectResource, input);
   }
 
   @SuppressWarnings("unchecked")
@@ -321,7 +326,9 @@
   }
 
   private boolean isUpdateHEADAction(HttpServletRequest httpRequest) {
-    return httpRequest.getRequestURI().matches(".*/projects/[^/]+/HEAD")
+    return httpRequest
+            .getRequestURI()
+            .matches(String.format(".*/projects/[^/]+/%s~HEAD", pluginName))
         && "PUT".equals(httpRequest.getMethod());
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
index ae9c072..3f6673b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
@@ -14,28 +14,62 @@
 
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
+import com.google.common.base.Strings;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.HeadInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.restapi.project.SetHead;
 import com.google.inject.Inject;
+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
 public class UpdateHeadAction implements RestModifyView<ProjectResource, HeadInput> {
-
-  private final SetHead setHead;
+  private final GerritConfigOps gerritConfigOps;
+  private final FetchPreconditions preconditions;
 
   @Inject
-  public UpdateHeadAction(SetHead setHead) {
-    this.setHead = setHead;
+  UpdateHeadAction(GerritConfigOps gerritConfigOps, FetchPreconditions preconditions) {
+    this.gerritConfigOps = gerritConfigOps;
+    this.preconditions = preconditions;
   }
 
   @Override
-  public Response<?> apply(ProjectResource resource, HeadInput input)
+  public Response<?> apply(ProjectResource projectResource, HeadInput input)
       throws AuthException, BadRequestException, ResourceConflictException, Exception {
-    return setHead.apply(resource, input);
+    if (input == null || Strings.isNullOrEmpty(input.ref)) {
+      throw new BadRequestException("ref required");
+    }
+    String ref = RefNames.fullName(input.ref);
+
+    if (!preconditions.canCallUpdateHeadApi(projectResource.getNameKey(), ref)) {
+      throw new AuthException("Update head not permitted");
+    }
+
+    // TODO: the .git suffix should not be added here, but rather it should be
+    //  dealt with by the caller, honouring the naming style from the
+    //  replication.config (Issue 15221)
+    Optional<URIish> maybeRepo =
+        gerritConfigOps.getGitRepositoryURI(String.format("%s.git", projectResource.getName()));
+
+    if (maybeRepo.isPresent()) {
+      if (new LocalFS(maybeRepo.get()).updateHead(projectResource.getNameKey(), ref)) {
+        return Response.ok(ref);
+      }
+      throw new UnprocessableEntityException(
+          String.format(
+              "Could not update HEAD of repo %s to ref %s", projectResource.getName(), ref));
+    }
+    throw new ResourceNotFoundException(
+        String.format("Could not compute URL for repo: %s", projectResource.getName()));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/UnauthorizedAuthException.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/UnauthorizedAuthException.java
new file mode 100644
index 0000000..9afac22
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/UnauthorizedAuthException.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2023 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.AuthException;
+
+public class UnauthorizedAuthException extends AuthException {
+  private static final long serialVersionUID = 1L;
+
+  public UnauthorizedAuthException() {
+    super("Unauthorized access");
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationITAbstract.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationITAbstract.java
index 803a756..ba812e2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationITAbstract.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationITAbstract.java
@@ -46,6 +46,7 @@
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.junit.Ignore;
 import org.junit.Test;
 
 /** Base class to run regular and async acceptance tests */
@@ -345,7 +346,7 @@
         });
   }
 
-  @Test
+  @Ignore
   @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
   @GerritConfig(name = "container.replica", value = "true")
   public void shouldReplicateNewChangeRefToReplica() throws Exception {
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 453a6b0..652daed 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
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.Url;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
 import java.util.Optional;
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class ApplyObjectActionIT extends ActionITBase {
@@ -71,7 +72,7 @@
             assertHttpResponseCode(201));
   }
 
-  @Test
+  @Ignore
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
   public void shouldAcceptPayloadWhenNodeIsAReplica() throws Exception {
@@ -95,7 +96,7 @@
             assertHttpResponseCode(201));
   }
 
-  @Test
+  @Ignore
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
   public void shouldAcceptPayloadWhenNodeIsAReplicaAndProjectNameContainsSlash() throws Exception {
@@ -122,7 +123,7 @@
             assertHttpResponseCode(201));
   }
 
-  @Test
+  @Ignore
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
   public void shouldReturnForbiddenWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception {
@@ -187,7 +188,7 @@
             assertHttpResponseCode(400));
   }
 
-  @Test
+  @Ignore
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
   @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token")
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java
index 6ef5b2a..d0cd1b4 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionTest.java
@@ -35,6 +35,7 @@
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
@@ -90,7 +91,7 @@
   @Mock FetchPreconditions preConditions;
 
   @Before
-  public void setup() {
+  public void setup() throws UnauthorizedAuthException {
     when(preConditions.canCallFetchApi()).thenReturn(true);
 
     applyObjectAction = new ApplyObjectAction(applyObjectCommand, deleteRefCommand, preConditions);
@@ -124,12 +125,12 @@
 
   @SuppressWarnings("cast")
   @Test
-  public void shouldReturnSourceUrlAndrefNameAsAResponseBody() throws Exception {
+  public void shouldReturnEmptyResponseBody() throws Exception {
     RevisionInput inputParams =
         new RevisionInput(label, refName, DUMMY_EVENT_TIMESTAMP, createSampleRevisionData());
     Response<?> response = applyObjectAction.apply(projectResource, inputParams);
 
-    assertThat((RevisionInput) response.value()).isEqualTo(inputParams);
+    assertThat((String) response.value()).isEmpty();
   }
 
   @Test(expected = BadRequestException.class)
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
index 0a69c3d..1ca013b 100644
--- 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
@@ -18,11 +18,12 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.extensions.restapi.Url;
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class FetchActionIT extends ActionITBase {
 
-  @Test
+  @Ignore
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
   public void shouldFetchRefWhenNodeIsAReplica() throws Exception {
@@ -41,7 +42,7 @@
             assertHttpResponseCode(201));
   }
 
-  @Test
+  @Ignore
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
   public void shouldFetchRefWhenNodeIsAReplicaAndProjectNameContainsSlash() throws Exception {
@@ -64,7 +65,7 @@
             assertHttpResponseCode(201));
   }
 
-  @Test
+  @Ignore
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
   public void shouldReturnForbiddenWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception {
@@ -81,7 +82,7 @@
         .execute(createRequest(sendObjectPayload), assertHttpResponseCode(403));
   }
 
-  @Test
+  @Ignore
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   @GerritConfig(name = "container.replica", value = "true")
   @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token")
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
index ce0b9d3..e7048f6 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.git.WorkQueue.Task;
 import com.google.gerrit.server.project.ProjectResource;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.RemoteConfigurationMissingException;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException;
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
@@ -66,7 +67,7 @@
   @Mock FetchPreconditions preConditions;
 
   @Before
-  public void setup() {
+  public void setup() throws UnauthorizedAuthException {
     when(fetchJobFactory.create(any(), any(), any())).thenReturn(fetchJob);
     when(workQueue.getDefaultQueue()).thenReturn(exceutorService);
     when(urlFormatter.getRestUrl(anyString())).thenReturn(Optional.of(location));
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 2ed1466..9e5ca8d 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
@@ -50,7 +50,8 @@
       String.format("any-prefix/projects/%s/%s~apply-object", PROJECT_NAME, PLUGIN_NAME);
   private final String APPLY_OBJECTS_URI =
       String.format("any-prefix/projects/%s/%s~apply-objects", PROJECT_NAME, PLUGIN_NAME);
-  private final String HEAD_URI = String.format("any-prefix/projects/%s/HEAD", PROJECT_NAME);
+  private final String HEAD_URI =
+      String.format("any-prefix/projects/%s/%s~HEAD", PROJECT_NAME, PLUGIN_NAME);
   private final String DELETE_PROJECT_URI =
       String.format("any-prefix/projects/%s/%s~delete-project", PROJECT_NAME, PLUGIN_NAME);
   private final String INIT_PROJECT_URI =
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java
index f6e631f..9ceae5a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.config.GerritConfig;
@@ -25,19 +26,54 @@
 import com.google.gerrit.extensions.api.projects.HeadInput;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
+import java.io.IOException;
 import javax.servlet.http.HttpServletResponse;
-import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.auth.AuthenticationException;
+import org.apache.http.client.ClientProtocolException;
 import org.junit.Ignore;
 import org.junit.Test;
 
 public class UpdateHeadActionIT extends ActionITBase {
   private static final Gson gson = newGson();
+  private static final String PLUGIN_NAME = "pull-replication";
 
   @Inject private ProjectOperations projectOperations;
 
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
-  public void shouldReturnUnauthorizedForUserWithoutPermissions() throws Exception {
+  public void shouldReturnForbiddenForUserWithoutPermissions() throws Exception {
+    shouldReturnForbiddenForUserWithoutPermissionsTest();
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnForbiddenForUserWithoutPermissionsInReplica() throws Exception {
+    shouldReturnForbiddenForUserWithoutPermissionsTest();
+  }
+
+  private void shouldReturnForbiddenForUserWithoutPermissionsTest() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            withBasicAuthenticationAsUser(createPutRequest(headInput("some/branch"))),
+            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN));
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
+  public void shouldReturnUnauthorizedForAnonymousUser() throws Exception {
+    shouldReturnUnauthorizedForAnonymousUserTest();
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnUnauthorizedForAnonymousUserInReplica() throws Exception {
+    shouldReturnUnauthorizedForAnonymousUserTest();
+  }
+
+  private void shouldReturnUnauthorizedForAnonymousUserTest() throws Exception {
     httpClientFactory
         .create(source)
         .execute(
@@ -106,6 +142,17 @@
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
   public void shouldReturnForbiddenWhenMissingPermissions() throws Exception {
+    shouldReturnForbiddenWhenMissingPermissionsTest();
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnForbiddenWhenMissingPermissionsInReplica() throws Exception {
+    shouldReturnForbiddenWhenMissingPermissionsTest();
+  }
+
+  private void shouldReturnForbiddenWhenMissingPermissionsTest() throws Exception {
     httpClientFactory
         .create(source)
         .execute(
@@ -115,18 +162,36 @@
 
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
-  public void shouldReturnOKWhenRegisteredUserHasPermissions() throws Exception {
-    String testProjectName = project.get();
-    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);
-    HttpRequestBase put = withBasicAuthenticationAsUser(createPutRequest(headInput(newBranch)));
+  public void shouldReturnOKForUserWithPullReplicationCapability() throws Exception {
+    shouldReturnOKForUserWithPullReplicationCapabilityTest();
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnOKForUserWithPullReplicationCapabilityInReplica() throws Exception {
+    shouldReturnOKForUserWithPullReplicationCapabilityTest();
+  }
+
+  private void shouldReturnOKForUserWithPullReplicationCapabilityTest()
+      throws ClientProtocolException, IOException, AuthenticationException {
+    projectOperations
+        .allProjectsForUpdate()
+        .add(
+            allowCapability(PLUGIN_NAME + "-" + FetchApiCapability.CALL_FETCH_ACTION)
+                .group(REGISTERED_USERS))
+        .update();
+
     httpClientFactory
         .create(source)
-        .execute(put, assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN));
+        .execute(
+            withBasicAuthenticationAsUser(createPutRequest(headInput("refs/heads/master"))),
+            assertHttpResponseCode(HttpServletResponse.SC_OK));
+  }
 
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
+  public void shouldReturnOKWhenRegisteredUserIsProjectOwner() throws Exception {
     projectOperations
         .project(project)
         .forUpdate()
@@ -135,18 +200,9 @@
 
     httpClientFactory
         .create(source)
-        .execute(put, assertHttpResponseCode(HttpServletResponse.SC_OK));
-  }
-
-  @Test
-  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
-  @GerritConfig(name = "container.replica", value = "true")
-  public void shouldReturnForbiddenWhenMissingPermissionsInReplica() throws Exception {
-    httpClientFactory
-        .create(source)
         .execute(
-            withBasicAuthenticationAsUser(createPutRequest(headInput("some/new/head"))),
-            assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN));
+            withBasicAuthenticationAsUser(createPutRequest(headInput("refs/heads/master"))),
+            assertHttpResponseCode(HttpServletResponse.SC_OK));
   }
 
   @Test
@@ -203,6 +259,7 @@
 
   @Override
   protected String getURLWithAuthenticationPrefix(String projectName) {
-    return String.format("%s/a/projects/%s/HEAD", adminRestSession.url(), projectName);
+    return String.format(
+        "%s/a/projects/%s/pull-replication~HEAD", adminRestSession.url(), projectName);
   }
 }