Use replication plugin API to update and access its config

Instead of directly modifying the replication plugin configuration files
in the `$site_path/etc` directory use new API. This guarantee that the
configuration will be stored and read in a correct format and location.

Bug: Issue 325000746
Change-Id: I9bde7e1148d932fb6861f51edb3521449ec20bef
diff --git a/github-plugin/BUILD b/github-plugin/BUILD
index 589f7c6..3ff3d06 100644
--- a/github-plugin/BUILD
+++ b/github-plugin/BUILD
@@ -30,6 +30,7 @@
     resources = glob(["src/main/resources/**/*"]),
     deps = [
         ":github-plugin-lib",
+        ":replication-api",
         "//plugins/github/github-oauth:github-oauth-lib",
         "@axis-jaxrpc//jar",
         "@axis//jar",
@@ -76,7 +77,9 @@
         ":github-plugin__plugin",
         "//javatests/com/google/gerrit/util/http/testutil",
         "//plugins/github/github-oauth:github-oauth-lib",
+        "//plugins/replication:replication-api",
         "@commons-io//jar",
+        "@github-api//jar",
     ],
 )
 
@@ -93,3 +96,9 @@
     neverlink = True,
     exports = ["@lombok//jar"],
 )
+
+java_library(
+    name = "replication-api",
+    neverlink = True,
+    exports = ["//plugins/replication:replication-api"],
+)
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceModule.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceModule.java
index 9bff585..991ba5a 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceModule.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceModule.java
@@ -18,18 +18,13 @@
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.events.EventListener;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gson.Gson;
 import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
-import com.googlesource.gerrit.plugins.github.git.FanoutReplicationConfig;
-import com.googlesource.gerrit.plugins.github.git.FileBasedReplicationConfig;
-import com.googlesource.gerrit.plugins.github.git.ReplicationConfig;
 import com.googlesource.gerrit.plugins.github.group.GitHubGroupBackend;
 import com.googlesource.gerrit.plugins.github.group.GitHubGroupMembership;
 import com.googlesource.gerrit.plugins.github.group.GitHubGroupsCache;
@@ -42,17 +37,9 @@
 import com.googlesource.gerrit.plugins.github.replication.ReplicationStatusFlatFile;
 import com.googlesource.gerrit.plugins.github.replication.ReplicationStatusListener;
 import com.googlesource.gerrit.plugins.github.replication.ReplicationStatusStore;
-import java.nio.file.Files;
 
 public class GuiceModule extends AbstractModule {
 
-  private final SitePaths site;
-
-  @Inject
-  public GuiceModule(SitePaths site) {
-    this.site = site;
-  }
-
   @Override
   protected void configure() {
     bind(new TypeLiteral<UserScopedProvider<GitHubLogin>>() {})
@@ -75,12 +62,6 @@
           }
         });
 
-    if (Files.exists(site.etc_dir.resolve("replication"))) {
-      bind(ReplicationConfig.class).to(FanoutReplicationConfig.class).in(Scopes.SINGLETON);
-    } else {
-      bind(ReplicationConfig.class).to(FileBasedReplicationConfig.class).in(Scopes.SINGLETON);
-    }
-
     bind(ReplicationStatusStore.class).to(ReplicationStatusFlatFile.class).in(Scopes.SINGLETON);
     bind(Gson.class).toProvider(GerritGsonProvider.class);
   }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FanoutReplicationConfig.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FanoutReplicationConfig.java
deleted file mode 100644
index 4a3bc4b..0000000
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FanoutReplicationConfig.java
+++ /dev/null
@@ -1,72 +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.github.git;
-
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-
-public class FanoutReplicationConfig implements ReplicationConfig {
-  private final SitePaths site;
-  private final FileBasedConfig secureConf;
-
-  @Inject
-  public FanoutReplicationConfig(final SitePaths site) {
-    this.site = site;
-    this.secureConf = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
-  }
-
-  @Override
-  public void addSecureCredentials(String authUsername, String authToken)
-      throws IOException, ConfigInvalidException {
-    secureConf.load();
-    secureConf.setString("remote", authUsername, "username", authUsername);
-    secureConf.setString("remote", authUsername, "password", authToken);
-    secureConf.save();
-  }
-
-  @Override
-  public void addReplicationRemote(String githubUsername, String url, String projectName)
-      throws IOException, ConfigInvalidException {
-
-    FileBasedConfig replicationConf =
-        new FileBasedConfig(
-            new File(site.etc_dir.toFile(), String.format("replication/%s.config", githubUsername)),
-            FS.DETECTED);
-
-    replicationConf.load();
-    String currentUrl = replicationConf.getString("remote", null, "url");
-    if (currentUrl == null) {
-      replicationConf.setString("remote", null, "url", url);
-    }
-    List<String> projects =
-        new ArrayList<>(Arrays.asList(replicationConf.getStringList("remote", null, "projects")));
-    projects.add(projectName);
-    replicationConf.setStringList("remote", null, "projects", projects);
-
-    String currentPushRefs = replicationConf.getString("remote", null, "push");
-    if (currentPushRefs == null) {
-      replicationConf.setString("remote", null, "push", "refs/*:refs/*");
-    }
-    replicationConf.save();
-  }
-}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FileBasedReplicationConfig.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FileBasedReplicationConfig.java
deleted file mode 100644
index 842bde3..0000000
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/FileBasedReplicationConfig.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2013 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.github.git;
-
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-
-public class FileBasedReplicationConfig implements ReplicationConfig {
-  private final FileBasedConfig secureConf;
-  private final FileBasedConfig replicationConf;
-
-  @Inject
-  public FileBasedReplicationConfig(final SitePaths site) {
-    replicationConf =
-        new FileBasedConfig(new File(site.etc_dir.toFile(), "replication.config"), FS.DETECTED);
-    secureConf = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
-  }
-
-  @Override
-  public synchronized void addSecureCredentials(String authUsername, String authToken)
-      throws IOException, ConfigInvalidException {
-    secureConf.load();
-    secureConf.setString("remote", authUsername, "username", authUsername);
-    secureConf.setString("remote", authUsername, "password", authToken);
-    secureConf.save();
-  }
-
-  @Override
-  public synchronized void addReplicationRemote(String username, String url, String projectName)
-      throws IOException, ConfigInvalidException {
-    replicationConf.load();
-    replicationConf.setString("remote", username, "url", url);
-    List<String> projects =
-        new ArrayList<>(
-            Arrays.asList(replicationConf.getStringList("remote", username, "projects")));
-    projects.add(projectName);
-    replicationConf.setStringList("remote", username, "projects", projects);
-    replicationConf.setString("remote", username, "push", "refs/*:refs/*");
-    replicationConf.save();
-  }
-}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicateProjectStep.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicateProjectStep.java
index 5109cd2..4e75d31 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicateProjectStep.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicateProjectStep.java
@@ -13,22 +13,21 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.github.git;
 
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.github.GitHubURL;
-import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
-import com.googlesource.gerrit.plugins.github.oauth.ScopedProvider;
+import com.googlesource.gerrit.plugins.replication.api.ReplicationRemotesApi;
 import java.io.IOException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class ReplicateProjectStep extends ImportStep {
   private static final Logger LOG = LoggerFactory.getLogger(ReplicateProjectStep.class);
-  private final ReplicationConfig replicationConfig;
-  private final String authUsername;
-  private final String authToken;
-  private final String gitHubUrl;
+  private final DynamicItem<ReplicationRemotesApi> replicationRemotesUpdaterItem;
+  private final ReplicationRemoteConfigBuilder remoteConfigBuilder;
 
   public interface Factory {
     ReplicateProjectStep create(
@@ -37,20 +36,17 @@
 
   @Inject
   public ReplicateProjectStep(
-      final ReplicationConfig replicationConfig,
+      final DynamicItem<ReplicationRemotesApi> replicationRemotesUpdaterItem,
       final GitHubRepository.Factory gitHubRepoFactory,
-      final ScopedProvider<GitHubLogin> ghLoginProvider,
+      ReplicationRemoteConfigBuilder remoteConfigBuilder,
       @GitHubURL String gitHubUrl,
       @Assisted("organisation") String organisation,
       @Assisted("name") String repository)
       throws IOException {
     super(gitHubUrl, organisation, repository, gitHubRepoFactory);
+    this.remoteConfigBuilder = remoteConfigBuilder;
     LOG.debug("Gerrit ReplicateProject " + organisation + "/" + repository);
-    this.replicationConfig = replicationConfig;
-    GitHubLogin ghLogin = ghLoginProvider.get();
-    this.authUsername = ghLogin.getMyself().getLogin();
-    this.authToken = ghLogin.getAccessToken();
-    this.gitHubUrl = gitHubUrl;
+    this.replicationRemotesUpdaterItem = replicationRemotesUpdaterItem;
   }
 
   @Override
@@ -58,11 +54,15 @@
     progress.beginTask("Setting up Gerrit replication", 2);
 
     String repositoryName = getOrganisation() + "/" + getRepositoryName();
+    Config remoteConfig = remoteConfigBuilder.build(repositoryName);
     progress.update(1);
-    replicationConfig.addSecureCredentials(authUsername, authToken);
+
+    ReplicationRemotesApi updater = replicationRemotesUpdaterItem.get();
+    if (updater != null) {
+      updater.update(remoteConfig);
+    }
     progress.update(1);
-    replicationConfig.addReplicationRemote(
-        authUsername, gitHubUrl + "/${name}.git", repositoryName);
+
     progress.endTask();
   }
 
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicationConfig.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicationConfig.java
deleted file mode 100644
index 8b7712a..0000000
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicationConfig.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2013 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.github.git;
-
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-public interface ReplicationConfig {
-
-  void addSecureCredentials(String authUsername, String authToken)
-      throws IOException, ConfigInvalidException;
-
-  void addReplicationRemote(String username, String url, String projectName)
-      throws IOException, ConfigInvalidException;
-}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicationRemoteConfigBuilder.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicationRemoteConfigBuilder.java
new file mode 100644
index 0000000..bef109c
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/git/ReplicationRemoteConfigBuilder.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2024 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.github.git;
+
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.github.GitHubURL;
+import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
+import com.googlesource.gerrit.plugins.github.oauth.ScopedProvider;
+import com.googlesource.gerrit.plugins.replication.api.ReplicationRemotesApi;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+
+class ReplicationRemoteConfigBuilder {
+  private final String gitHubUrl;
+  private final String username;
+  private final String authToken;
+  private final DynamicItem<ReplicationRemotesApi> replicationConfigItem;
+
+  @Inject
+  ReplicationRemoteConfigBuilder(
+      DynamicItem<ReplicationRemotesApi> replicationRemotesItem,
+      ScopedProvider<GitHubLogin> ghLoginProvider,
+      @GitHubURL String gitHubUrl)
+      throws IOException {
+    this.gitHubUrl = gitHubUrl;
+    this.replicationConfigItem = replicationRemotesItem;
+    GitHubLogin ghLogin = ghLoginProvider.get();
+    this.username = ghLogin.getMyself().getLogin();
+    this.authToken = ghLogin.getAccessToken();
+  }
+
+  Config build(String repositoryName) {
+    Config remoteConfig = new Config();
+
+    remoteConfig.setString("remote", username, "username", username);
+    remoteConfig.setString("remote", username, "password", authToken);
+
+    remoteConfig.setString("remote", username, "url", gitHubUrl + "/${name}.git");
+
+    String[] existingProjects = getProjects();
+    List<String> projects = new ArrayList<>(List.of(existingProjects));
+    projects.add(repositoryName);
+
+    remoteConfig.setStringList("remote", username, "projects", projects);
+    remoteConfig.setString("remote", username, "push", "refs/*:refs/*");
+
+    return remoteConfig;
+  }
+
+  private String[] getProjects() {
+    ReplicationRemotesApi config = replicationConfigItem.get();
+    if (config != null) {
+      return config.get(username).getStringList("remote", username, "projects");
+    }
+
+    return new String[0];
+  }
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java
index 810632a..6d24b3e 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java
@@ -18,22 +18,17 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
-import java.io.IOException;
+import com.googlesource.gerrit.plugins.replication.api.ReplicationConfig;
 import java.net.URISyntaxException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteConfig;
 import org.eclipse.jgit.transport.URIish;
-import org.eclipse.jgit.util.FS;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -60,15 +55,15 @@
 
   @Inject
   GitHubDestinations(
-      final SitePaths site,
+      final ReplicationConfig replicationConfig,
       final RemoteSiteUser.Factory ruf,
       final GroupBackend gb,
       final PluginUser pu)
-      throws ConfigInvalidException, IOException {
+      throws ConfigInvalidException {
     pluginUser = pu;
     replicationUserFactory = ruf;
     groupBackend = gb;
-    configs = getDestinations(site.etc_dir.resolve("replication.config"));
+    configs = getDestinations(replicationConfig.getConfig());
     organisations = getOrganisations(configs);
   }
 
@@ -83,22 +78,7 @@
     return result;
   }
 
-  private List<Destination> getDestinations(Path cfgPath)
-      throws ConfigInvalidException, IOException {
-    if (!Files.exists(cfgPath) || Files.size(cfgPath) == 0) {
-      return Collections.emptyList();
-    }
-
-    FileBasedConfig cfg = new FileBasedConfig(cfgPath.toFile(), FS.DETECTED);
-    try {
-      cfg.load();
-    } catch (ConfigInvalidException e) {
-      throw new ConfigInvalidException(
-          String.format("Config file %s is invalid: %s", cfg.getFile(), e.getMessage()), e);
-    } catch (IOException e) {
-      throw new IOException(String.format("Cannot read %s: %s", cfg.getFile(), e.getMessage()), e);
-    }
-
+  private List<Destination> getDestinations(Config cfg) throws ConfigInvalidException {
     ImmutableList.Builder<Destination> dest = ImmutableList.builder();
     for (RemoteConfig c : allRemotes(cfg)) {
       if (c.getURIs().isEmpty()) {
@@ -108,9 +88,7 @@
       for (URIish u : c.getURIs()) {
         if (u.getPath() == null || !u.getPath().contains("${name}")) {
           throw new ConfigInvalidException(
-              String.format(
-                  "remote.%s.url \"%s\" lacks ${name} placeholder in %s",
-                  c.getName(), u, cfg.getFile()));
+              String.format("remote.%s.url \"%s\" lacks ${name} placeholder", c.getName(), u));
         }
       }
 
@@ -131,7 +109,7 @@
     return dest.build();
   }
 
-  private static List<RemoteConfig> allRemotes(FileBasedConfig cfg) throws ConfigInvalidException {
+  private static List<RemoteConfig> allRemotes(Config cfg) throws ConfigInvalidException {
     Set<String> names = cfg.getSubsections("remote");
     List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size());
     for (String name : names) {
@@ -140,8 +118,7 @@
           result.add(new RemoteConfig(cfg, name));
         }
       } catch (URISyntaxException e) {
-        throw new ConfigInvalidException(
-            String.format("remote %s has invalid URL in %s", name, cfg.getFile()));
+        throw new ConfigInvalidException(String.format("remote %s has invalid URL", name));
       }
     }
     return result;
diff --git a/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/FanoutReplicationConfigTest.java b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/FanoutReplicationConfigTest.java
deleted file mode 100644
index 3613c41..0000000
--- a/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/FanoutReplicationConfigTest.java
+++ /dev/null
@@ -1,99 +0,0 @@
-// 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.github;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.server.config.SitePaths;
-import com.googlesource.gerrit.plugins.github.git.FanoutReplicationConfig;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import org.apache.commons.io.FileUtils;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class FanoutReplicationConfigTest {
-
-  private static final String CUSTOM_KEY = "mykey";
-  private static final String CUSTOM_VALUE = "myvalue";
-  private static final String REMOTE_ENDPOINT = "my-remote-endpoint";
-  private static final String TEST_REMOTE_URL = "http://github.com/myurl";
-  private static final String TEST_PROJECT_NAME = "myprojectname";
-  private Path tempDir;
-  private SitePaths sitePaths;
-
-  @Before
-  public void setup() throws Exception {
-    tempDir = Files.createTempDirectory(getClass().getSimpleName());
-    sitePaths = new SitePaths(tempDir);
-    Files.createDirectories(sitePaths.etc_dir);
-  }
-
-  @After
-  public void teardown() throws Exception {
-    FileUtils.deleteDirectory(tempDir.toFile());
-  }
-
-  @Test
-  public void shoudKeepAdHocSettingsInFanoutReplicationConfig() throws Exception {
-    FileBasedConfig currConfig = getReplicationConfig();
-    currConfig.setString("remote", null, CUSTOM_KEY, CUSTOM_VALUE);
-    currConfig.save();
-
-    String url = "http://github.com/myurl";
-    FanoutReplicationConfig fanoutReplicationConfig = new FanoutReplicationConfig(sitePaths);
-    fanoutReplicationConfig.addReplicationRemote(REMOTE_ENDPOINT, url, "myproject");
-
-    currConfig.load();
-    assertThat(currConfig.getString("remote", null, CUSTOM_KEY)).isEqualTo(CUSTOM_VALUE);
-  }
-
-  @Test
-  public void shoudKeepCustomUrlInFanoutReplicationConfig() throws Exception {
-    FileBasedConfig currConfig = getReplicationConfig();
-    String customUrl = "http://my-custom-url";
-    currConfig.setString("remote", null, "url", customUrl);
-    currConfig.save();
-
-    new FanoutReplicationConfig(sitePaths)
-        .addReplicationRemote(REMOTE_ENDPOINT, TEST_REMOTE_URL, TEST_PROJECT_NAME);
-
-    currConfig.load();
-    assertThat(currConfig.getString("remote", null, "url")).isEqualTo(customUrl);
-  }
-
-  @Test
-  public void shoudKeepCustomPushRefSpecInFanoutReplicationConfig() throws Exception {
-    FileBasedConfig currConfig = getReplicationConfig();
-    String customPushRefSpec = "+refs/heads/myheads/*:refs/heads/myheads/*";
-    currConfig.setString("remote", null, "push", customPushRefSpec);
-    currConfig.save();
-
-    new FanoutReplicationConfig(sitePaths)
-        .addReplicationRemote(REMOTE_ENDPOINT, TEST_REMOTE_URL, TEST_PROJECT_NAME);
-
-    currConfig.load();
-    assertThat(currConfig.getString("remote", null, "push")).isEqualTo(customPushRefSpec);
-  }
-
-  private FileBasedConfig getReplicationConfig() {
-    return new FileBasedConfig(
-        sitePaths.etc_dir.resolve("replication").resolve(REMOTE_ENDPOINT + ".config").toFile(),
-        FS.DETECTED);
-  }
-}
diff --git a/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/git/ReplicationRemoteConfigBuilderTest.java b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/git/ReplicationRemoteConfigBuilderTest.java
new file mode 100644
index 0000000..c6023e0
--- /dev/null
+++ b/github-plugin/src/test/java/com/googlesource/gerrit/plugins/github/git/ReplicationRemoteConfigBuilderTest.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2024 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.github.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
+import com.googlesource.gerrit.plugins.github.oauth.ScopedProvider;
+import com.googlesource.gerrit.plugins.replication.api.ReplicationRemotesApi;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.kohsuke.github.GHMyself;
+
+public class ReplicationRemoteConfigBuilderTest {
+  private final String repoName = "test-repo";
+  private final String username = "test-user";
+  private final String password = "myHighlySecretPassword";
+  private final String gitHubUrl = "htpps://github.com";
+
+  @Test
+  public void shouldBuildConfig() throws Exception {
+    ReplicationRemoteConfigBuilder builder = newReplicationRemoteConfigBuilder();
+    Config actual = builder.build(repoName);
+
+    assertThat(actual.getString("remote", username, "username")).isEqualTo(username);
+    assertThat(actual.getString("remote", username, "password")).isEqualTo(password);
+    assertThat(actual.getString("remote", username, "url")).isEqualTo(gitHubUrl + "/${name}.git");
+    assertThat(actual.getStringList("remote", username, "projects"))
+        .isEqualTo(new String[] {repoName});
+    assertThat(actual.getString("remote", username, "push")).isEqualTo("refs/*:refs/*");
+  }
+
+  @Test
+  public void shouldAppendProjectToConfig() throws Exception {
+    String prevProject = "imported-project";
+    Config currentConfig = new Config();
+    currentConfig.setString("remote", username, "projects", prevProject);
+
+    ReplicationRemoteConfigBuilder builder = newReplicationRemoteConfigBuilder(currentConfig);
+    Config actual = builder.build(repoName);
+
+    assertThat(actual.getString("remote", username, "username")).isEqualTo(username);
+    assertThat(actual.getString("remote", username, "password")).isEqualTo(password);
+    assertThat(actual.getString("remote", username, "url")).isEqualTo(gitHubUrl + "/${name}.git");
+    assertThat(actual.getStringList("remote", username, "projects"))
+        .isEqualTo(new String[] {prevProject, repoName});
+    assertThat(actual.getString("remote", username, "push")).isEqualTo("refs/*:refs/*");
+  }
+
+  private ReplicationRemoteConfigBuilder newReplicationRemoteConfigBuilder() throws Exception {
+    return newReplicationRemoteConfigBuilder(new Config());
+  }
+
+  private ReplicationRemoteConfigBuilder newReplicationRemoteConfigBuilder(Config currentConfig)
+      throws Exception {
+    GitHubLogin gitHubLoginMock = mock(GitHubLogin.class);
+    GHMyself ghMyselfMock = mock(GHMyself.class);
+    ScopedProvider<GitHubLogin> scopedProviderMock = mock(ScopedProvider.class);
+    ReplicationRemotesApi replicationRemotesApi = mock(ReplicationRemotesApi.class);
+    DynamicItem<ReplicationRemotesApi> replicationRemotesItem = mock(DynamicItem.class);
+
+    when(ghMyselfMock.getLogin()).thenReturn(username);
+    when(gitHubLoginMock.getMyself()).thenReturn(ghMyselfMock);
+    when(gitHubLoginMock.getAccessToken()).thenReturn(password);
+    when(scopedProviderMock.get()).thenReturn(gitHubLoginMock);
+    when(replicationRemotesApi.get(username)).thenReturn(currentConfig);
+    when(replicationRemotesItem.get()).thenReturn(replicationRemotesApi);
+
+    return new ReplicationRemoteConfigBuilder(
+        replicationRemotesItem, scopedProviderMock, gitHubUrl);
+  }
+}