Test replication config and auto-reload

Unit-test the loading of the configuration through the
ReplicationFileBasedConfig and the auto-reload mechanism
provided by the AutoReloadConfigDecorator.

Change-Id: I9cf613c325d6adf5593a7ed7627b9f1774eb582d
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java
new file mode 100644
index 0000000..da533c6
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/AbstractConfigTest.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2019 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.truth.Truth.assertThat;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.easymock.IAnswer;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.Before;
+import org.junit.Ignore;
+
+@Ignore
+public abstract class AbstractConfigTest {
+  protected final Path sitePath;
+  protected final SitePaths sitePaths;
+  protected final Destination.Factory destinationFactoryMock;
+  protected final Path pluginDataPath;
+
+  static class FakeDestination extends Destination {
+    public final DestinationConfiguration config;
+
+    protected FakeDestination(DestinationConfiguration config) {
+      super(injectorMock(), null, null, null, null, null, null, null, null, null, config);
+      this.config = config;
+    }
+
+    private static Injector injectorMock() {
+      Injector injector = createNiceMock(Injector.class);
+      Injector childInjectorMock = createNiceMock(Injector.class);
+      expect(injector.createChildInjector((Module) anyObject())).andReturn(childInjectorMock);
+      replay(childInjectorMock);
+      replay(injector);
+      return injector;
+    }
+  }
+
+  AbstractConfigTest() throws IOException {
+    sitePath = createTempPath("site");
+    sitePaths = new SitePaths(sitePath);
+    pluginDataPath = createTempPath("data");
+    destinationFactoryMock = createMock(Destination.Factory.class);
+  }
+
+  @Before
+  public void setup() {
+    expect(destinationFactoryMock.create(isA(DestinationConfiguration.class)))
+        .andAnswer(
+            new IAnswer<Destination>() {
+              @Override
+              public Destination answer() throws Throwable {
+                return new FakeDestination((DestinationConfiguration) getCurrentArguments()[0]);
+              }
+            })
+        .anyTimes();
+    replay(destinationFactoryMock);
+  }
+
+  protected static Path createTempPath(String prefix) throws IOException {
+    return java.nio.file.Files.createTempDirectory(prefix);
+  }
+
+  protected FileBasedConfig newReplicationConfig() {
+    FileBasedConfig replicationConfig =
+        new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED);
+    return replicationConfig;
+  }
+
+  protected void assertThatIsDestination(
+      Destination destination, String remoteName, String... remoteUrls) {
+    DestinationConfiguration destinationConfig = ((FakeDestination) destination).config;
+    assertThat(destinationConfig.getRemoteConfig().getName()).isEqualTo(remoteName);
+    assertThat(destinationConfig.getUrls()).containsExactlyElementsIn(remoteUrls);
+  }
+
+  protected void assertThatContainsDestination(
+      List<Destination> destinations, String remoteName, String... remoteUrls) {
+    List<Destination> matchingDestinations =
+        destinations.stream()
+            .filter(
+                (Destination dst) ->
+                    ((FakeDestination) dst).config.getRemoteConfig().getName().equals(remoteName))
+            .collect(Collectors.toList());
+
+    assertThat(matchingDestinations).isNotEmpty();
+
+    assertThatIsDestination(matchingDestinations.get(0), remoteName, remoteUrls);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java
new file mode 100644
index 0000000..1b83625
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/AutoReloadConfigDecoratorTest.java
@@ -0,0 +1,251 @@
+// Copyright (C) 2019 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.truth.Truth.assertThat;
+import static org.easymock.EasyMock.anyInt;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.util.Providers;
+import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AutoReloadConfigDecoratorTest extends AbstractConfigTest {
+  private AutoReloadConfigDecorator autoReloadConfig;
+  private ReplicationQueue replicationQueueMock;
+  private WorkQueue workQueueMock;
+  private FakeExecutorService executorService = new FakeExecutorService();
+
+  public class FakeExecutorService implements ScheduledExecutorService {
+    public Runnable refreshCommand;
+
+    @Override
+    public void shutdown() {}
+
+    @Override
+    public List<Runnable> shutdownNow() {
+      return null;
+    }
+
+    @Override
+    public boolean isShutdown() {
+      return false;
+    }
+
+    @Override
+    public boolean isTerminated() {
+      return false;
+    }
+
+    @Override
+    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+      return false;
+    }
+
+    @Override
+    public <T> Future<T> submit(Callable<T> task) {
+      return null;
+    }
+
+    @Override
+    public <T> Future<T> submit(Runnable task, T result) {
+      return null;
+    }
+
+    @Override
+    public Future<?> submit(Runnable task) {
+      return null;
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
+        throws InterruptedException {
+      return null;
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(
+        Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
+        throws InterruptedException {
+      return null;
+    }
+
+    @Override
+    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
+        throws InterruptedException, ExecutionException {
+      return null;
+    }
+
+    @Override
+    public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
+        throws InterruptedException, ExecutionException, TimeoutException {
+      return null;
+    }
+
+    @Override
+    public void execute(Runnable command) {}
+
+    @Override
+    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+      return null;
+    }
+
+    @Override
+    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+      return null;
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleAtFixedRate(
+        Runnable command, long initialDelay, long period, TimeUnit unit) {
+      refreshCommand = command;
+      return null;
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleWithFixedDelay(
+        Runnable command, long initialDelay, long delay, TimeUnit unit) {
+      return null;
+    }
+  }
+
+  public AutoReloadConfigDecoratorTest() throws IOException {
+    super();
+  }
+
+  @Override
+  @Before
+  public void setup() {
+    super.setup();
+
+    setupMocks();
+  }
+
+  private void setupMocks() {
+    replicationQueueMock = createNiceMock(ReplicationQueue.class);
+    replay(replicationQueueMock);
+
+    workQueueMock = createNiceMock(WorkQueue.class);
+    expect(workQueueMock.createQueue(anyInt(), anyObject(String.class))).andReturn(executorService);
+    replay(workQueueMock);
+  }
+
+  @Test
+  public void shouldLoadNotEmptyInitialReplicationConfig() throws Exception {
+    FileBasedConfig replicationConfig = newReplicationConfig();
+    String remoteName = "foo";
+    String remoteUrl = "ssh://git@git.somewhere.com/${name}";
+    replicationConfig.setString("remote", remoteName, "url", remoteUrl);
+    replicationConfig.save();
+
+    autoReloadConfig =
+        new AutoReloadConfigDecorator(
+            sitePaths,
+            destinationFactoryMock,
+            Providers.of(replicationQueueMock),
+            pluginDataPath,
+            "replication",
+            workQueueMock);
+
+    List<Destination> destinations = autoReloadConfig.getDestinations(FilterType.ALL);
+    assertThat(destinations).hasSize(1);
+    assertThatIsDestination(destinations.get(0), remoteName, remoteUrl);
+  }
+
+  @Test
+  public void shouldAutoReloadReplicationConfig() throws Exception {
+    FileBasedConfig replicationConfig = newReplicationConfig();
+    replicationConfig.setBoolean("gerrit", null, "autoReload", true);
+    String remoteName1 = "foo";
+    String remoteUrl1 = "ssh://git@git.foo.com/${name}";
+    replicationConfig.setString("remote", remoteName1, "url", remoteUrl1);
+    replicationConfig.save();
+
+    autoReloadConfig =
+        new AutoReloadConfigDecorator(
+            sitePaths,
+            destinationFactoryMock,
+            Providers.of(replicationQueueMock),
+            pluginDataPath,
+            "replication",
+            workQueueMock);
+    autoReloadConfig.startup(workQueueMock);
+
+    List<Destination> destinations = autoReloadConfig.getDestinations(FilterType.ALL);
+    assertThat(destinations).hasSize(1);
+    assertThatIsDestination(destinations.get(0), remoteName1, remoteUrl1);
+
+    TimeUnit.SECONDS.sleep(1); // Allow the filesystem to change the update TS
+
+    String remoteName2 = "bar";
+    String remoteUrl2 = "ssh://git@git.bar.com/${name}";
+    replicationConfig.setString("remote", remoteName2, "url", remoteUrl2);
+    replicationConfig.save();
+    executorService.refreshCommand.run();
+
+    destinations = autoReloadConfig.getDestinations(FilterType.ALL);
+    assertThat(destinations).hasSize(2);
+    assertThatContainsDestination(destinations, remoteName1, remoteUrl1);
+    assertThatContainsDestination(destinations, remoteName2, remoteUrl2);
+  }
+
+  @Test
+  public void shouldNotAutoReloadReplicationConfigIfDisabled() throws Exception {
+    String remoteName1 = "foo";
+    String remoteUrl1 = "ssh://git@git.foo.com/${name}";
+    FileBasedConfig replicationConfig = newReplicationConfig();
+    replicationConfig.setBoolean("gerrit", null, "autoReload", false);
+    replicationConfig.setString("remote", remoteName1, "url", remoteUrl1);
+    replicationConfig.save();
+
+    autoReloadConfig =
+        new AutoReloadConfigDecorator(
+            sitePaths,
+            destinationFactoryMock,
+            Providers.of(replicationQueueMock),
+            pluginDataPath,
+            "replication",
+            workQueueMock);
+    autoReloadConfig.startup(workQueueMock);
+
+    List<Destination> destinations = autoReloadConfig.getDestinations(FilterType.ALL);
+    assertThat(destinations).hasSize(1);
+    assertThatIsDestination(destinations.get(0), remoteName1, remoteUrl1);
+
+    TimeUnit.SECONDS.sleep(1); // Allow the filesystem to change the update TS
+
+    replicationConfig.setString("remote", "bar", "url", "ssh://git@git.bar.com/${name}");
+    replicationConfig.save();
+    executorService.refreshCommand.run();
+
+    assertThat(autoReloadConfig.getDestinations(FilterType.ALL)).isEqualTo(destinations);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfigTest.java
new file mode 100644
index 0000000..36cc209
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/ReplicationFileBasedConfigTest.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2019 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.truth.Truth.assertThat;
+
+import com.googlesource.gerrit.plugins.replication.ReplicationConfig.FilterType;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.junit.Test;
+
+public class ReplicationFileBasedConfigTest extends AbstractConfigTest {
+
+  public ReplicationFileBasedConfigTest() throws IOException {
+    super();
+  }
+
+  @Test
+  public void shouldLoadOneDestination() throws Exception {
+    String remoteName = "foo";
+    String remoteUrl = "ssh://git@git.somewhere.com/${name}";
+    FileBasedConfig config = newReplicationConfig();
+    config.setString("remote", remoteName, "url", remoteUrl);
+    config.save();
+
+    ReplicationFileBasedConfig replicationConfig = newReplicationFileBasedConfig();
+    List<Destination> destinations = replicationConfig.getDestinations(FilterType.ALL);
+    assertThat(destinations).hasSize(1);
+
+    assertThatIsDestination(destinations.get(0), remoteName, remoteUrl);
+  }
+
+  @Test
+  public void shouldLoadTwoDestinations() throws Exception {
+    String remoteName1 = "foo";
+    String remoteUrl1 = "ssh://git@git.somewhere.com/${name}";
+    String remoteName2 = "bar";
+    String remoteUrl2 = "ssh://git@git.elsewhere.com/${name}";
+    FileBasedConfig config = newReplicationConfig();
+    config.setString("remote", remoteName1, "url", remoteUrl1);
+    config.setString("remote", remoteName2, "url", remoteUrl2);
+    config.save();
+
+    ReplicationFileBasedConfig replicationConfig = newReplicationFileBasedConfig();
+    List<Destination> destinations = replicationConfig.getDestinations(FilterType.ALL);
+    assertThat(destinations).hasSize(2);
+
+    assertThatIsDestination(destinations.get(0), remoteName1, remoteUrl1);
+    assertThatIsDestination(destinations.get(1), remoteName2, remoteUrl2);
+  }
+
+  private ReplicationFileBasedConfig newReplicationFileBasedConfig()
+      throws ConfigInvalidException, IOException {
+    ReplicationFileBasedConfig replicationConfig =
+        new ReplicationFileBasedConfig(sitePaths, destinationFactoryMock, pluginDataPath);
+    return replicationConfig;
+  }
+}