Merge branch 'stable-3.4'

* stable-3.4:
  Add missing project creation support for replicas
  Add missing project creation support

Change-Id: I5ae4aa1da1edef05d47b3f7af55969851c24449d
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 3ded8eb..2a77e09 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
@@ -216,6 +216,18 @@
           FetchRestApiClient fetchClient = fetchClientFactory.create(source);
 
           HttpResult result = fetchClient.callSendObject(project, refName, revision, uri);
+          if (!result.isSuccessful()
+              && source.isCreateMissingRepositories()
+              && result.isProjectMissing(project)) {
+            HttpResult initProjectResult = fetchClient.initProject(project, uri);
+            if (initProjectResult.isSuccessful()) {
+              result = fetchClient.callFetch(project, "refs/*", uri);
+            } else {
+              String errorMessage =
+                  initProjectResult.getMessage().map(e -> " - Error: " + e).orElse("");
+              repLog.error("Cannot create project " + project + errorMessage);
+            }
+          }
           if (!result.isSuccessful()) {
             repLog.warn(
                 String.format(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
index 9346ce5..b8bee26 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
@@ -732,6 +732,10 @@
     return config.getMaxRetries();
   }
 
+  public boolean isCreateMissingRepositories() {
+    return config.createMissingRepositories();
+  }
+
   private static boolean matches(URIish uri, String urlMatch) {
     if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) {
       return true;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
index 8046d49..4858b17 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfiguration.java
@@ -38,6 +38,7 @@
   private final int poolThreads;
   private final boolean replicatePermissions;
   private final boolean replicateHiddenProjects;
+  private final boolean createMissingRepositories;
   private final String remoteNameStyle;
   private final ImmutableList<String> urls;
   private final ImmutableList<String> projects;
@@ -74,6 +75,7 @@
     authGroupNames = ImmutableList.copyOf(cfg.getStringList("remote", name, "authGroup"));
     lockErrorMaxRetries = cfg.getInt("replication", "lockErrorMaxRetries", 0);
 
+    createMissingRepositories = cfg.getBoolean("remote", name, "createMissingRepositories", true);
     replicatePermissions = cfg.getBoolean("remote", name, "replicatePermissions", true);
     replicateHiddenProjects = cfg.getBoolean("remote", name, "replicateHiddenProjects", false);
     useCGitClient = cfg.getBoolean("replication", "useCGitClient", false);
@@ -191,6 +193,10 @@
     return maxRetries;
   }
 
+  public boolean createMissingRepositories() {
+    return createMissingRepositories;
+  }
+
   private static int getInt(RemoteConfig rc, Config cfg, String name, int defValue) {
     return cfg.getInt("remote", rc.getName(), name, defValue);
   }
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 7ae7d54..b2ef28d 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
@@ -35,6 +35,8 @@
       DynamicSet.bind(binder(), AllRequestFilter.class)
           .to(PullReplicationFilter.class)
           .in(Scopes.SINGLETON);
+    } else {
+      serveRegex("/init-project/.*$").with(ProjectInitializationAction.class);
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpServletOps.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpServletOps.java
new file mode 100644
index 0000000..b424555
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpServletOps.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull.api;
+
+import static com.google.common.net.HttpHeaders.ACCEPT;
+import static org.eclipse.jgit.util.HttpSupport.TEXT_PLAIN;
+
+import com.google.common.net.MediaType;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+class HttpServletOps {
+
+  static boolean checkAcceptHeader(HttpServletRequest req, HttpServletResponse rsp)
+      throws IOException {
+    if (req.getHeader(ACCEPT) == null
+        || (req.getHeader(ACCEPT) != null
+            && !Arrays.asList(
+                    MediaType.PLAIN_TEXT_UTF_8.toString(),
+                    MediaType.ANY_TEXT_TYPE.toString(),
+                    MediaType.ANY_TYPE.toString())
+                .contains(req.getHeader(ACCEPT)))) {
+      setResponse(
+          rsp,
+          HttpServletResponse.SC_BAD_REQUEST,
+          "No advertised 'Accept' headers can be honoured. 'text/plain' should be provided in the request 'Accept' header.");
+      return false;
+    }
+
+    return true;
+  }
+
+  static void setResponse(HttpServletResponse httpResponse, int statusCode, String value)
+      throws IOException {
+    httpResponse.setContentType(TEXT_PLAIN);
+    httpResponse.setStatus(statusCode);
+    PrintWriter writer = httpResponse.getWriter();
+    writer.print(value);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
new file mode 100644
index 0000000..8f5c9d0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
@@ -0,0 +1,125 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull.api;
+
+import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION;
+import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.checkAcceptHeader;
+import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.setResponse;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.replication.LocalFS;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.util.Optional;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.URIish;
+
+@Singleton
+public class ProjectInitializationAction extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static final String PROJECT_NAME = "project-name";
+
+  private final SitePaths sitePath;
+  private final Config gerritConfig;
+  private final Provider<CurrentUser> userProvider;
+
+  @Inject
+  ProjectInitializationAction(
+      @GerritServerConfig Config cfg, SitePaths sitePath, Provider<CurrentUser> userProvider) {
+    this.sitePath = sitePath;
+    this.gerritConfig = cfg;
+    this.userProvider = userProvider;
+  }
+
+  @Override
+  protected void doPut(
+      HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
+      throws ServletException, IOException {
+
+    if (!checkAcceptHeader(httpServletRequest, httpServletResponse)) {
+      return;
+    }
+
+    if (!userProvider.get().isIdentifiedUser()) {
+      setResponse(
+          httpServletResponse,
+          HttpServletResponse.SC_UNAUTHORIZED,
+          "Unauthorized user. '" + CALL_FETCH_ACTION + "' capability needed.");
+      return;
+    }
+
+    String path = httpServletRequest.getRequestURI();
+    String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1));
+
+    if (initProject(projectName)) {
+      setResponse(
+          httpServletResponse,
+          HttpServletResponse.SC_CREATED,
+          "Project " + projectName + " initialized");
+      return;
+    }
+
+    setResponse(
+        httpServletResponse,
+        HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+        "Cannot initialize project " + projectName);
+  }
+
+  protected boolean initProject(String projectName) {
+    Optional<URIish> maybeUri = getGitRepositoryURI(projectName);
+    if (!maybeUri.isPresent()) {
+      logger.atSevere().log("Cannot initialize project '{}'", projectName);
+      return false;
+    }
+    LocalFS localFS = new LocalFS(maybeUri.get());
+    Project.NameKey projectNameKey = Project.NameKey.parse(projectName);
+    return localFS.createProject(projectNameKey, RefNames.HEAD);
+  }
+
+  private Optional<URIish> getGitRepositoryURI(String projectName) {
+    Path basePath = sitePath.resolve(gerritConfig.getString("gerrit", null, "basePath"));
+    URIish uri;
+
+    try {
+      uri = new URIish("file://" + basePath + "/" + projectName);
+      return Optional.of(uri);
+    } catch (URISyntaxException e) {
+      logger.atSevere().withCause(e).log("Unsupported URI for project " + projectName);
+    }
+
+    return Optional.empty();
+  }
+
+  public static String getProjectInitializationUrl(String pluginName, String projectName) {
+    return String.format(
+        "a/plugins/%s/init-project/%s", pluginName, Url.encode(projectName) + ".git");
+  }
+}
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 65b8e1b..1425be6 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
@@ -15,10 +15,13 @@
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
 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;
 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;
 
@@ -28,10 +31,12 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.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.extensions.restapi.Url;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.json.OutputFormat;
@@ -48,6 +53,7 @@
 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.exception.InitProjectException;
 import java.io.BufferedReader;
 import java.io.EOFException;
 import java.io.IOException;
@@ -67,6 +73,7 @@
 
   private FetchAction fetchAction;
   private ApplyObjectAction applyObjectAction;
+  private ProjectInitializationAction projectInitializationAction;
   private ProjectsCollection projectsCollection;
   private Gson gson;
   private Provider<CurrentUser> userProvider;
@@ -75,10 +82,12 @@
   public PullReplicationFilter(
       FetchAction fetchAction,
       ApplyObjectAction applyObjectAction,
+      ProjectInitializationAction projectInitializationAction,
       ProjectsCollection projectsCollection,
       Provider<CurrentUser> userProvider) {
     this.fetchAction = fetchAction;
     this.applyObjectAction = applyObjectAction;
+    this.projectInitializationAction = projectInitializationAction;
     this.projectsCollection = projectsCollection;
     this.userProvider = userProvider;
     this.gson = OutputFormat.JSON.newGsonBuilder().create();
@@ -107,6 +116,15 @@
         } else {
           httpResponse.sendError(SC_UNAUTHORIZED);
         }
+      } else if (isInitProjectAction(httpRequest)) {
+        if (userProvider.get().isIdentifiedUser()) {
+          if (!checkAcceptHeader(httpRequest, httpResponse)) {
+            return;
+          }
+          doInitProject(httpRequest, httpResponse);
+        } else {
+          httpResponse.sendError(SC_UNAUTHORIZED);
+        }
       } else {
         chain.doFilter(request, response);
       }
@@ -126,11 +144,27 @@
     } catch (ResourceConflictException e) {
       RestApiServlet.replyError(
           httpRequest, httpResponse, SC_CONFLICT, e.getMessage(), e.caching(), e);
+    } catch (InitProjectException | ResourceNotFoundException e) {
+      RestApiServlet.replyError(
+          httpRequest, httpResponse, SC_INTERNAL_SERVER_ERROR, e.getMessage(), e.caching(), e);
     } catch (Exception e) {
       throw new ServletException(e);
     }
   }
 
+  private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
+      throws RestApiException, IOException {
+
+    String path = httpRequest.getRequestURI();
+    String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1));
+    if (projectInitializationAction.initProject(projectName)) {
+      setResponse(
+          httpResponse, HttpServletResponse.SC_CREATED, "Project " + projectName + " initialized");
+      return;
+    }
+    throw new InitProjectException(projectName);
+  }
+
   @SuppressWarnings("unchecked")
   private Response<Map<String, Object>> doApplyObject(HttpServletRequest httpRequest)
       throws RestApiException, IOException, PermissionBackendException {
@@ -222,4 +256,8 @@
   private boolean isFetchAction(HttpServletRequest httpRequest) {
     return httpRequest.getRequestURI().endsWith("pull-replication~fetch");
   }
+
+  private boolean isInitProjectAction(HttpServletRequest httpRequest) {
+    return httpRequest.getRequestURI().contains("pull-replication/init-project/");
+  }
 }
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
new file mode 100644
index 0000000..85a7729
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/exception/InitProjectException.java
@@ -0,0 +1,25 @@
+// 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/client/FetchRestApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
index 7b876df..811d064 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
@@ -15,6 +15,7 @@
 package com.googlesource.gerrit.plugins.replication.pull.client;
 
 import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
+import static com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction.getProjectInitializationUrl;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Strings;
@@ -44,6 +45,7 @@
 import org.apache.http.client.CredentialsProvider;
 import org.apache.http.client.ResponseHandler;
 import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
 import org.apache.http.client.protocol.HttpClientContext;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.BasicCredentialsProvider;
@@ -109,6 +111,16 @@
     return httpClientFactory.create(source).execute(post, this, getContext(targetUri));
   }
 
+  public HttpResult initProject(Project.NameKey project, URIish uri) throws IOException {
+    String url =
+        String.format(
+            "%s/%s", uri.toString(), getProjectInitializationUrl(pluginName, project.get()));
+    HttpPut put = new HttpPut(url);
+    put.addHeader(new BasicHeader("Accept", MediaType.ANY_TEXT_TYPE.toString()));
+    put.addHeader(new BasicHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString()));
+    return httpClientFactory.create(source).execute(put, this, getContext(uri));
+  }
+
   public HttpResult callSendObject(
       Project.NameKey project, String refName, RevisionData revisionData, URIish targetUri)
       throws ClientProtocolException, IOException {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResult.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResult.java
index dc01295..bd164df 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResult.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpResult.java
@@ -19,6 +19,7 @@
 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 
+import com.google.gerrit.entities.Project;
 import java.util.Optional;
 
 public class HttpResult {
@@ -38,6 +39,11 @@
     return responseCode == SC_CREATED || responseCode == SC_NO_CONTENT || responseCode == SC_OK;
   }
 
+  public boolean isProjectMissing(Project.NameKey projectName) {
+    String projectMissingMessage = String.format("Not found: %s", projectName.get());
+    return message.map(msg -> msg.contains(projectMissingMessage)).orElse(false);
+  }
+
   public boolean isParentObjectMissing() {
     return responseCode == SC_CONFLICT;
   }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index d4c5abf..f2a596b 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -349,6 +349,11 @@
 
 	By default, 1 thread.
 
+remote.NAME.createMissingRepositories
+:	Replicate newly created repositories.
+
+	By default, true.
+
 remote.NAME.authGroup
 :	Specifies the name of a group that the remote should use to
 	access the repositories. Multiple authGroups may be specified
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 327c6ba..286fe24 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
@@ -17,6 +17,7 @@
 import static java.nio.file.Files.createTempDirectory;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
@@ -95,6 +96,7 @@
         .thenReturn(httpResult);
     when(fetchRestApiClient.callFetch(any(), anyString(), any())).thenReturn(httpResult);
     when(httpResult.isSuccessful()).thenReturn(true);
+    when(httpResult.isProjectMissing(any())).thenReturn(false);
 
     objectUnderTest =
         new ReplicationQueue(wq, rd, dis, sl, fetchClientFactory, refsFilter, revReader);
@@ -110,6 +112,32 @@
   }
 
   @Test
+  public void shouldCallInitProjectWhenProjectIsMissing() throws IOException {
+    Event event = new TestEvent("refs/changes/01/1/meta");
+    when(httpResult.isSuccessful()).thenReturn(false);
+    when(httpResult.isProjectMissing(any())).thenReturn(true);
+    when(source.isCreateMissingRepositories()).thenReturn(true);
+
+    objectUnderTest.start();
+    objectUnderTest.onGitReferenceUpdated(event);
+
+    verify(fetchRestApiClient).initProject(any(), any());
+  }
+
+  @Test
+  public void shouldNotCallInitProjectWhenReplicateNewRepositoriesNotSet() throws IOException {
+    Event event = new TestEvent("refs/changes/01/1/meta");
+    when(httpResult.isSuccessful()).thenReturn(false);
+
+    when(source.isCreateMissingRepositories()).thenReturn(false);
+
+    objectUnderTest.start();
+    objectUnderTest.onGitReferenceUpdated(event);
+
+    verify(fetchRestApiClient, never()).initProject(any(), any());
+  }
+
+  @Test
   public void shouldCallSendObjectWhenPatchSetRef() throws ClientProtocolException, IOException {
     Event event = new TestEvent("refs/changes/01/1/1");
     objectUnderTest.start();
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
new file mode 100644
index 0000000..919ac34
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull.api;
+
+import static com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction.getProjectInitializationUrl;
+
+import com.google.common.net.MediaType;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.extensions.restapi.Url;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.message.BasicHeader;
+import org.junit.Test;
+
+public class ProjectInitializationActionIT extends ActionITBase {
+  public static final String INVALID_TEST_PROJECT_NAME = "\0";
+  private String testProjectName = "new/Project";
+
+  @Test
+  public void shouldReturnUnauthorizedForUserWithoutPermissions() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED),
+            getAnonymousContext());
+  }
+
+  @Test
+  public void shouldReturnBadRequestIfContentNotSet() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithoutHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST),
+            getContext());
+  }
+
+  @Test
+  public void shouldCreateRepository() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_CREATED),
+            getContext());
+
+    HttpGet getNewProjectRequest =
+        new HttpGet(userRestSession.url() + "/a/projects/" + Url.encode("new/Project"));
+    httpClientFactory
+        .create(source)
+        .execute(
+            getNewProjectRequest, assertHttpResponseCode(HttpServletResponse.SC_OK), getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldCreateRepositoryWhenNodeIsAReplica() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_CREATED),
+            getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnInternalServerErrorIfProjectCannotBeCreatedWhenNodeIsAReplica()
+      throws Exception {
+    testProjectName = INVALID_TEST_PROJECT_NAME;
+    url = getURL();
+
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR),
+            getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnBadRequestIfContentNotSetWhenNodeIsAReplica() throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithoutHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST),
+            getContext());
+  }
+
+  @Test
+  @GerritConfig(name = "container.replica", value = "true")
+  public void shouldReturnUnauthorizedForUserWithoutPermissionsWhenNodeIsAReplica()
+      throws Exception {
+    httpClientFactory
+        .create(source)
+        .execute(
+            createPutRequestWithHeaders(),
+            assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED),
+            getAnonymousContext());
+  }
+
+  @Override
+  protected String getURL() {
+    return userRestSession.url()
+        + "/"
+        + getProjectInitializationUrl("pull-replication", Url.encode(testProjectName));
+  }
+
+  protected HttpPut createPutRequestWithHeaders() {
+    HttpPut put = createPutRequestWithoutHeaders();
+    put.addHeader(new BasicHeader("Accept", MediaType.ANY_TEXT_TYPE.toString()));
+    put.addHeader(new BasicHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString()));
+    return put;
+  }
+
+  protected HttpPut createPutRequestWithoutHeaders() {
+    HttpPut put = new HttpPut(url);
+    return put;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
index a3b0b02..39a8f2d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
@@ -39,6 +39,7 @@
 import org.apache.http.Header;
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
 import org.apache.http.message.BasicHeader;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -67,6 +68,7 @@
   @Mock ReplicationFileBasedConfig replicationConfig;
   @Mock Source source;
   @Captor ArgumentCaptor<HttpPost> httpPostCaptor;
+  @Captor ArgumentCaptor<HttpPut> httpPutCaptor;
 
   String api = "http://gerrit-host";
   String pluginName = "pull-replication";
@@ -347,6 +349,19 @@
                 source));
   }
 
+  @Test
+  public void shouldCallInitProjectEndpoint() throws IOException, URISyntaxException {
+
+    objectUnderTest.initProject(Project.nameKey("test_repo"), new URIish(api));
+
+    verify(httpClient, times(1)).execute(httpPutCaptor.capture(), any(), any());
+
+    HttpPut httpPut = httpPutCaptor.getValue();
+    assertThat(httpPut.getURI().getHost()).isEqualTo("gerrit-host");
+    assertThat(httpPut.getURI().getPath())
+        .isEqualTo("/a/plugins/pull-replication/init-project/test_repo.git");
+  }
+
   public String readPayload(HttpPost entity) throws UnsupportedOperationException, IOException {
     ByteBuffer buf = IO.readWholeStream(entity.getEntity().getContent(), 1024);
     return RawParseUtils.decode(buf.array(), buf.arrayOffset(), buf.limit()).trim();