Expose API to update remotes' configurations Introduce `ReplicationRemotesUpdater` class to add or update remotes configuration programmatically. To propagate changes to different configuration resources, both `ConfigResource` and `ReplicationConfigOverrides` got new method `void udpate(Config config)`. That lets persist changes in other configuration resources. In order to make Guice happy with all of the required bindings, some were moved from `ReplicationModule` to `ReplicationConfigModule`. Finally, the `ReplicationRemotesUpdater`, will use `SecureStore` to persist `remote.$name.password` in a secure manner. For end-to-end testing replication-remote-updates[1] can be used. [1] https://github.com/dluksza/replication-remote-updates Bug: Issue 322146238 Change-Id: I8003ec6c827f865eb383092db8f07033e92da488
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ConfigResource.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ConfigResource.java index 7b41fe6..c0a5a7d 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ConfigResource.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ConfigResource.java
@@ -14,6 +14,7 @@ package com.googlesource.gerrit.plugins.replication; +import java.io.IOException; import org.eclipse.jgit.lib.Config; /** @@ -37,6 +38,16 @@ Config getConfig(); /** + * Update the configuration resource. + * + * <p>Allows to persist changes to the configuration resource. + * + * @param config updated configuration + * @throws IOException when configuration cannot be persisted + */ + void update(Config config) throws IOException; + + /** * Current logical version string of the current configuration loaded in memory, depending on the * actual implementation of the configuration on the persistent storage. *
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResource.java b/src/main/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResource.java index 4220ddb..0a2afa6 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResource.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResource.java
@@ -23,9 +23,11 @@ import com.google.common.hash.Hashing; import com.google.gerrit.server.config.SitePaths; import com.google.inject.Inject; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; @@ -61,6 +63,30 @@ } } + @Override + public void update(Config updates) throws IOException { + Set<String> remotes = updates.getSubsections("remote"); + for (String remote : remotes) { + File remoteFile = remoteConfigsDirPath.resolve(remote + ".config").toFile(); + FileBasedConfig remoteConfig = new FileBasedConfig(remoteFile, FS.DETECTED); + try { + remoteConfig.load(); + } catch (ConfigInvalidException e) { + throw new IOException( + String.format("Cannot parse configuration file: %s", remoteFile.getAbsoluteFile()), e); + } + Set<String> options = updates.getNames("remote", remote); + for (String option : options) { + List<String> values = List.of(updates.getStringList("remote", remote, option)); + remoteConfig.setStringList("remote", remote, option, values); + } + remoteConfig.save(); + } + + removeRemotes(updates); + super.update(updates); + } + private static void removeRemotes(Config config) { Set<String> remoteNames = config.getSubsections("remote"); if (remoteNames.size() > 0) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/FileConfigResource.java b/src/main/java/com/googlesource/gerrit/plugins/replication/FileConfigResource.java index e5bd8d0..7e19d2c 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/FileConfigResource.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/FileConfigResource.java
@@ -17,12 +17,14 @@ import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; import com.google.gerrit.common.UsedAt; import com.google.gerrit.common.UsedAt.Project; import com.google.gerrit.server.config.SitePaths; import com.google.inject.Inject; import java.io.IOException; import java.nio.file.Path; +import java.util.List; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.storage.file.FileBasedConfig; @@ -53,6 +55,25 @@ } @Override + public void update(Config updates) throws IOException { + for (String section : updates.getSections()) { + for (String subsection : updates.getSubsections(section)) { + for (String name : updates.getNames(section, subsection, true)) { + List<String> values = + Lists.newArrayList(updates.getStringList(section, subsection, name)); + config.setStringList(section, subsection, name, values); + } + } + + for (String name : updates.getNames(section, true)) { + List<String> values = Lists.newArrayList(updates.getStringList(section, null, name)); + config.setStringList(section, null, name, values); + } + } + config.save(); + } + + @Override public String getVersion() { return Long.toString(config.getFile().lastModified()); }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java index 93a2b85..114ad15 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationConfigModule.java
@@ -23,11 +23,14 @@ import com.google.inject.Inject; import com.google.inject.ProvisionException; import com.google.inject.Scopes; +import com.google.inject.assistedinject.FactoryModuleBuilder; import com.google.inject.internal.UniqueAnnotations; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; + +import com.googlesource.gerrit.plugins.replication.events.ProjectDeletionState; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.util.FS; @@ -55,6 +58,14 @@ } else { bind(ReplicationConfig.class).to(ReplicationConfigImpl.class).in(Scopes.SINGLETON); } + + bind(ReplicationQueue.class).in(Scopes.SINGLETON); + bind(ObservableQueue.class).to(ReplicationQueue.class); + bind(ReplicationDestinations.class).to(DestinationsCollection.class); + bind(ConfigParser.class).to(DestinationConfigParser.class).in(Scopes.SINGLETON); + + install(new FactoryModuleBuilder().build(Destination.Factory.class)); + install(new FactoryModuleBuilder().build(ProjectDeletionState.Factory.class)); } public FileBasedConfig getReplicationConfig() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java index ac27280..a357a20 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationModule.java
@@ -52,10 +52,7 @@ @Override protected void configure() { - install(new FactoryModuleBuilder().build(Destination.Factory.class)); install(configModule); - bind(ReplicationQueue.class).in(Scopes.SINGLETON); - bind(ObservableQueue.class).to(ReplicationQueue.class); bind(LifecycleListener.class) .annotatedWith(UniqueAnnotations.create()) .to(ReplicationQueue.class); @@ -77,10 +74,8 @@ .to(StartReplicationCapability.class); install(new FactoryModuleBuilder().build(PushAll.Factory.class)); - install(new FactoryModuleBuilder().build(ProjectDeletionState.Factory.class)); bind(EventBus.class).in(Scopes.SINGLETON); - bind(ReplicationDestinations.class).to(DestinationsCollection.class); bind(ConfigParser.class).to(DestinationConfigParser.class).in(Scopes.SINGLETON); DynamicSet.setOf(binder(), ReplicationStateListener.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationRemotesUpdater.java b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationRemotesUpdater.java new file mode 100644 index 0000000..8c27ff6 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationRemotesUpdater.java
@@ -0,0 +1,108 @@ +// 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.replication; + +import static com.google.gerrit.common.UsedAt.Project.PLUGIN_GITHUB; + +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.UsedAt; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.server.securestore.SecureStore; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.List; +import org.eclipse.jgit.lib.Config; + +/** Public API to update replication plugin remotes configurations programmatically. */ +@Singleton +@UsedAt(PLUGIN_GITHUB) +public class ReplicationRemotesUpdater { + + private final SecureStore secureStore; + private final Provider<ConfigResource> baseConfigProvider; + private final DynamicItem<ReplicationConfigOverrides> configOverridesItem; + + @Inject + ReplicationRemotesUpdater( + SecureStore secureStore, + Provider<ConfigResource> baseConfigProvider, + @Nullable DynamicItem<ReplicationConfigOverrides> configOverridesItem) { + this.secureStore = secureStore; + this.baseConfigProvider = baseConfigProvider; + this.configOverridesItem = configOverridesItem; + } + + /** + * Adds or updates the remote configuration for the replication plugin. + * + * <p>Provided JGit {@link Config} object should contain at least one named <em>remote</em> + * section. All other configurations will be ignored. + * + * <p>NOTE: The {@code remote.$name.password} will be stored using {@link SecureStore}. + * + * @param remoteConfig remotes to add or update + * @throws IOException when persisting fails + */ + public void update(Config remoteConfig) throws IOException { + if (remoteConfig.getSubsections("remote").isEmpty()) { + throw new IllegalArgumentException( + "configuration update must have at least one 'remote' section"); + } + + SeparatedRemoteConfigs configs = onlyRemoteSectionsWithSeparatedPasswords(remoteConfig); + persistRemotesPasswords(configs); + + if (hasConfigOverrides()) { + configOverridesItem.get().update(configs.remotes); + } else { + baseConfigProvider.get().update(configs.remotes); + } + } + + private SeparatedRemoteConfigs onlyRemoteSectionsWithSeparatedPasswords(Config configUpdates) { + SeparatedRemoteConfigs configs = new SeparatedRemoteConfigs(); + for (String subSection : configUpdates.getSubsections("remote")) { + for (String name : configUpdates.getNames("remote", subSection)) { + List<String> values = List.of(configUpdates.getStringList("remote", subSection, name)); + if ("password".equals(name)) { + configs.passwords.setStringList("remote", subSection, "password", values); + } else { + configs.remotes.setStringList("remote", subSection, name, values); + } + } + } + + return configs; + } + + private void persistRemotesPasswords(SeparatedRemoteConfigs configs) { + for (String subSection : configs.passwords.getSubsections("remote")) { + List<String> values = + List.of(configs.passwords.getStringList("remote", subSection, "password")); + secureStore.setList("remote", subSection, "password", values); + } + } + + private boolean hasConfigOverrides() { + return configOverridesItem != null && configOverridesItem.get() != null; + } + + private static class SeparatedRemoteConfigs { + private final Config remotes = new Config(); + private final Config passwords = new Config(); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResourceTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResourceTest.java index 6782700..61f81df 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResourceTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/FanoutConfigResourceTest.java
@@ -16,12 +16,15 @@ import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE; import static com.google.common.truth.Truth.assertThat; +import static com.googlesource.gerrit.plugins.replication.FanoutConfigResource.CONFIG_DIR; import com.google.common.io.MoreFiles; import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType; +import java.io.File; import java.io.IOException; import java.util.List; import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.util.FS; import org.junit.Before; @@ -39,13 +42,18 @@ String remoteUrl2 = "ssh://git@git.elsewhere.com/${name}"; @Before - public void setupTests() { + public void setupTests() throws Exception { FileBasedConfig config = newReplicationConfig(); try { config.save(); } catch (IOException e) { throw new RuntimeException(e); } + File replicationConfig = sitePaths.etc_dir.resolve(CONFIG_DIR).toFile(); + if (!replicationConfig.mkdir()) { + throw new IOException( + "Cannot create test replication config directory in: " + replicationConfig.toPath().toAbsolutePath()); + } } @Test @@ -277,9 +285,69 @@ assertThat(objectUnderTest.getVersion()).isEqualTo(replicationConfigVersion); } + @Test + public void shouldAddConfigOptionToMainConfig() throws Exception { + FanoutConfigResource objectUnderTest = new FanoutConfigResource(sitePaths); + Config update = new Config(); + update.setString("new", null, "value", "set"); + + objectUnderTest.update(update); + Config updatedConfig = objectUnderTest.getConfig(); + + assertThat(updatedConfig.getString("new", null, "value")).isEqualTo("set"); + } + + @Test + public void shouldUpdateConfigOptionInMainConfig() throws Exception { + FileBasedConfig config = newReplicationConfig(); + config.setString("updatable", null, "value", "orig"); + config.save(); + FanoutConfigResource objectUnderTest = new FanoutConfigResource(sitePaths); + Config update = new Config(); + update.setString("updatable", null, "value", "updated"); + + objectUnderTest.update(update); + Config updatedConfig = objectUnderTest.getConfig(); + + assertThat(updatedConfig.getString("updatable", null, "value")).isEqualTo("updated"); + } + + @Test + public void shouldAddNewRemoteFile() throws Exception { + FanoutConfigResource objectUnderTest = new FanoutConfigResource(sitePaths); + Config update = new Config(); + update.setString("remote", remoteName1, "url", remoteUrl1); + + objectUnderTest.update(update); + + Config actual = loadRemoteConfig(remoteName1); + assertThat(actual.getString("remote", remoteName1, "url")).isEqualTo(remoteUrl1); + } + + @Test + public void shouldUpdateExistingRemote() throws Exception { + FileBasedConfig rawRemoteConfig = newRemoteConfig(remoteName1); + rawRemoteConfig.setString("remote", remoteName1, "url", remoteUrl1); + rawRemoteConfig.save(); + FanoutConfigResource objectUnderTest = new FanoutConfigResource(sitePaths); + Config update = new Config(); + update.setString("remote", remoteName1, "url", remoteUrl2); + + objectUnderTest.update(update); + + Config actual = loadRemoteConfig(remoteName1); + assertThat(actual.getString("remote", remoteName1, "url")).isEqualTo(remoteUrl2); + } + protected FileBasedConfig newRemoteConfig(String configFileName) { return new FileBasedConfig( sitePaths.etc_dir.resolve("replication/" + configFileName + ".config").toFile(), FS.DETECTED); } + + private Config loadRemoteConfig(String siteName) throws Exception { + FileBasedConfig config = newRemoteConfig(siteName); + config.load(); + return config; + } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/FileConfigResourceTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/FileConfigResourceTest.java new file mode 100644 index 0000000..9e8d36c --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/FileConfigResourceTest.java
@@ -0,0 +1,120 @@ +// 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.replication; + +import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.io.MoreFiles; +import com.google.gerrit.server.config.SitePaths; +import java.nio.file.Files; +import java.nio.file.Path; +import org.eclipse.jgit.lib.Config; +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 FileConfigResourceTest { + private static final String VALUE_KEY = "value"; + private static final String INITIAL_KEY = "initial"; + private static final String UPDATABLE_KEY = "updatable"; + + private Path testDir; + private SitePaths sitePaths; + private ConfigResource configResource; + + @Before + public void setUp() throws Exception { + testDir = Files.createTempDirectory("fileConfigResourceTest"); + sitePaths = new SitePaths(testDir); + configResource = newFileConfigResource(); + } + + @After + public void tearDown() throws Exception { + MoreFiles.deleteRecursively(testDir, ALLOW_INSECURE); + } + + @Test + public void updateEmptyFile() throws Exception { + Config configUpdate = newConfigUpdate(); + + Config beforeUpdate = configResource.getConfig(); + assertThat(beforeUpdate.getSections()).isEmpty(); + + configResource.update(configUpdate); + Config updatedConfig = newFileConfigResource().getConfig(); + + assertConfigUpdate(updatedConfig); + } + + @Test + public void appendOptionToConfig() throws Exception { + FileBasedConfig rawConfig = getRawReplicationConfig(); + rawConfig.setInt(INITIAL_KEY, null, VALUE_KEY, 10); + rawConfig.save(); + configResource = newFileConfigResource(); + Config configUpdate = newConfigUpdate(); + + configResource.update(configUpdate); + Config updatedConfig = configResource.getConfig(); + + assertConfigUpdate(updatedConfig); + assertThat(updatedConfig.getInt(INITIAL_KEY, null, VALUE_KEY, -1)).isEqualTo(10); + } + + @Test + public void updateExistingOption() throws Exception { + int expectedValue = 20; + Config configUpdate = new Config(); + configUpdate.setInt(UPDATABLE_KEY, null, VALUE_KEY, expectedValue); + FileBasedConfig rawConfig = getRawReplicationConfig(newConfigUpdate()); + rawConfig.save(); + + configResource.update(configUpdate); + Config updatedConfig = configResource.getConfig(); + + assertConfigUpdate(updatedConfig, expectedValue); + } + + private FileConfigResource newFileConfigResource() { + return new FileConfigResource(sitePaths); + } + + private Config newConfigUpdate() { + Config configUpdate = new Config(); + configUpdate.setInt(UPDATABLE_KEY, null, VALUE_KEY, 1); + return configUpdate; + } + + private void assertConfigUpdate(Config config) { + assertConfigUpdate(config, 1); + } + + private void assertConfigUpdate(Config config, int expectedValue) { + assertThat(config.getInt(UPDATABLE_KEY, null, VALUE_KEY, -1)).isEqualTo(expectedValue); + } + + private FileBasedConfig getRawReplicationConfig() { + return getRawReplicationConfig(new Config()); + } + + private FileBasedConfig getRawReplicationConfig(Config base) { + Path configPath = sitePaths.etc_dir.resolve(FileConfigResource.CONFIG_NAME); + return new FileBasedConfig(base, configPath.toFile(), FS.DETECTED); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/MergedConfigResourceTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/MergedConfigResourceTest.java index 86da1c0..c9ce2c5 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/MergedConfigResourceTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/MergedConfigResourceTest.java
@@ -19,6 +19,7 @@ import com.google.gerrit.extensions.registration.DynamicItem; import com.google.inject.AbstractModule; import com.google.inject.Guice; +import org.apache.commons.lang3.NotImplementedException; import org.eclipse.jgit.lib.Config; import org.junit.Test; @@ -75,6 +76,11 @@ } @Override + public void update(Config config) { + throw new NotImplementedException("not implemented"); + } + + @Override public String getVersion() { return "base"; } @@ -90,6 +96,11 @@ } @Override + public void update(Config config) { + throw new NotImplementedException("not implemented"); + } + + @Override public String getVersion() { return "override"; }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationRemotesUpdaterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationRemotesUpdaterTest.java new file mode 100644 index 0000000..6e57ba7 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationRemotesUpdaterTest.java
@@ -0,0 +1,149 @@ +// 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.replication; + +import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE; +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.testing.GerritJUnit.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.io.MoreFiles; +import com.google.common.truth.StringSubject; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.securestore.SecureStore; +import com.google.inject.util.Providers; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.eclipse.jgit.lib.Config; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ReplicationRemotesUpdaterTest { + + private Path testSite; + private SecureStore secureStoreMock; + private FileConfigResource baseConfig; + + @Before + public void setUp() throws Exception { + testSite = Files.createTempDirectory("replicationRemotesUpdateTest"); + secureStoreMock = mock(SecureStore.class); + baseConfig = new FileConfigResource(new SitePaths(testSite)); + } + + @After + public void tearDown() throws Exception { + MoreFiles.deleteRecursively(testSite, ALLOW_INSECURE); + } + + @Test + public void shouldThrowWhenNoRemotesInTheUpdate() { + Config update = new Config(); + ReplicationRemotesUpdater objectUnderTest = newReplicationConfigUpdater(); + + assertThrows(IllegalArgumentException.class, () -> objectUnderTest.update(update)); + + update.setString("non-remote", null, "value", "one"); + + assertThrows(IllegalArgumentException.class, () -> objectUnderTest.update(update)); + } + + @Test + public void addRemoteSectionToBaseConfigWhenNoOverrides() throws Exception { + String url = "fake_url"; + Config update = new Config(); + setRemoteSite(update, "url", url); + ReplicationRemotesUpdater objectUnderTest = newReplicationConfigUpdater(); + + objectUnderTest.update(update); + + assertRemoteSite(baseConfig.getConfig(), "url").isEqualTo(url); + } + + @Test + public void addRemoteSectionToBaseOverridesConfig() throws Exception { + TestReplicationConfigOverrides testOverrides = new TestReplicationConfigOverrides(); + String url = "fake_url"; + Config update = new Config(); + setRemoteSite(update, "url", url); + ReplicationRemotesUpdater objectUnderTest = newReplicationConfigUpdater(testOverrides); + + objectUnderTest.update(update); + + assertRemoteSite(testOverrides.getConfig(), "url").isEqualTo(url); + assertRemoteSite(baseConfig.getConfig(), "url").isNull(); + } + + @Test + public void encryptPassword() throws Exception { + TestReplicationConfigOverrides testOverrides = new TestReplicationConfigOverrides(); + Config update = new Config(); + String password = "my_secret_password"; + setRemoteSite(update, "password", password); + ReplicationRemotesUpdater objectUnderTest = newReplicationConfigUpdater(testOverrides); + + objectUnderTest.update(update); + + verify(secureStoreMock).setList("remote", "site", "password", List.of(password)); + assertRemoteSite(baseConfig.getConfig(), "password").isNull(); + assertRemoteSite(testOverrides.getConfig(), "password").isNull(); + } + + private ReplicationRemotesUpdater newReplicationConfigUpdater() { + return newReplicationConfigUpdater(null); + } + + private void setRemoteSite(Config config, String name, String value) { + config.setString("remote", "site", name, value); + } + + private StringSubject assertRemoteSite(Config config, String name) { + return assertThat(config.getString("remote", "site", name)); + } + + private ReplicationRemotesUpdater newReplicationConfigUpdater( + ReplicationConfigOverrides overrides) { + DynamicItem<ReplicationConfigOverrides> dynamicItemMock = mock(DynamicItem.class); + when(dynamicItemMock.get()).thenReturn(overrides); + + return new ReplicationRemotesUpdater( + secureStoreMock, Providers.of(baseConfig), dynamicItemMock); + } + + static class TestReplicationConfigOverrides implements ReplicationConfigOverrides { + private Config config = new Config(); + + @Override + public Config getConfig() { + return config; + } + + @Override + public void update(Config update) throws IOException { + config = update; + } + + @Override + public String getVersion() { + return "none"; + } + } +}