Merge branch 'stable-3.5' into stable-3.6

* stable-3.5:
  Add missing binding of ProjectDeletedListener
  Fix the Docker-based setup for the HA test environment
  Verify high-availability formatting using GJF 1.7

Change-Id: Id5248ae07bd66a11fc3d17f42868c2f5397a205b
diff --git a/BUILD b/BUILD
index 2ee0faa..7f32bc1 100644
--- a/BUILD
+++ b/BUILD
@@ -47,5 +47,7 @@
         ":high-availability__plugin",
         "@global-refdb//jar",
         "@wiremock//jar",
+        "@jgroups//jar",
+        "@commons-net//jar"
     ],
 )
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index da477e6..eaa632e 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -15,6 +15,6 @@
 
     maven_jar(
         name = "global-refdb",
-        artifact = "com.gerritforge:global-refdb:3.5.4",
-        sha1 = "6f96965d4cedd8b01b1fd9047d8c443c752bd675",
+        artifact = "com.gerritforge:global-refdb:3.6.3.1",
+        sha1 = "0f5229856d6a17e9c2382c8a404f43851f1f0287",
     )
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java
index 70ef751..4a3f473 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/AccountReindexRunnable.java
@@ -54,7 +54,7 @@
   protected Optional<Timestamp> indexIfNeeded(AccountState as, Timestamp sinceTs) {
     try {
       Account a = as.account();
-      Timestamp accountTs = a.registeredOn();
+      Timestamp accountTs = Timestamp.from(a.registeredOn());
       if (accountTs.after(sinceTs)) {
         log.atInfo().log("Index %s/%s/%s/%s", a.id(), a.fullName(), a.preferredEmail(), accountTs);
         accountIdx.index(a.id(), Operation.INDEX, Optional.empty());
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java
index 2de2122..aa44770 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnable.java
@@ -101,7 +101,7 @@
   @Override
   protected Optional<Timestamp> indexIfNeeded(Change c, Timestamp sinceTs) {
     try {
-      Timestamp changeTs = c.getLastUpdatedOn();
+      Timestamp changeTs = Timestamp.from(c.getLastUpdatedOn());
       if (changeTs.after(sinceTs)) {
         log.atInfo().log(
             "Index %s/%s/%s was updated after %s", c.getProject(), c.getId(), changeTs, sinceTs);
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java
index 4ded55e..05efe07 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnable.java
@@ -30,6 +30,7 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
@@ -73,7 +74,7 @@
       Optional<InternalGroup> internalGroup = groups.getGroup(g.getUUID());
       if (internalGroup.isPresent()) {
         InternalGroup group = internalGroup.get();
-        Timestamp groupCreationTs = group.getCreatedOn();
+        Timestamp groupCreationTs = Timestamp.from(group.getCreatedOn());
 
         Repository allUsersRepo = repoManager.openRepository(allUsers);
 
@@ -81,23 +82,24 @@
             groups.getSubgroupsAudit(allUsersRepo, g.getUUID());
         Stream<Timestamp> groupIdAudAddedTs =
             subGroupMembersAud.stream()
-                .map(AccountGroupByIdAudit::addedOn)
+                .map(accountGroupByIdAudit -> Timestamp.from(accountGroupByIdAudit.addedOn()))
                 .filter(Objects::nonNull);
         Stream<Timestamp> groupIdAudRemovedTs =
             subGroupMembersAud.stream()
                 .map(AccountGroupByIdAudit::removedOn)
-                .filter(Optional<Timestamp>::isPresent)
-                .map(Optional<Timestamp>::get);
-
+                .filter(Optional<Instant>::isPresent)
+                .map(inst -> Timestamp.from(inst.get()));
         List<AccountGroupMemberAudit> groupMembersAud =
             groups.getMembersAudit(allUsersRepo, g.getUUID());
         Stream<Timestamp> groupMemberAudAddedTs =
-            groupMembersAud.stream().map(AccountGroupMemberAudit::addedOn).filter(Objects::nonNull);
+            groupMembersAud.stream()
+                .map(accountGroupByIdAudit -> Timestamp.from(accountGroupByIdAudit.addedOn()))
+                .filter(Objects::nonNull);
         Stream<Timestamp> groupMemberAudRemovedTs =
             groupMembersAud.stream()
                 .map(AccountGroupMemberAudit::removedOn)
-                .filter(Optional<Timestamp>::isPresent)
-                .map(Optional<Timestamp>::get);
+                .filter(Optional<Instant>::isPresent)
+                .map(inst -> Timestamp.from(inst.get()));
 
         Optional<Timestamp> groupLastTs =
             Streams.concat(
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java
index a6539e0..d05c45d 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/IndexTs.java
@@ -32,6 +32,7 @@
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.sql.Timestamp;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Optional;
@@ -168,7 +169,8 @@
                 IndexName.CHANGE,
                 !changeNotes.isPresent()
                     ? LocalDateTime.now()
-                    : changeNotes.get().getChange().getLastUpdatedOn().toLocalDateTime());
+                    : Timestamp.from(changeNotes.get().getChange().getLastUpdatedOn())
+                        .toLocalDateTime());
           } catch (Exception e) {
             log.atWarning().withCause(e).log("Unable to update the latest TS for change %d", id);
           }
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java
index 3cb0ed0..92ec127 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ReindexRunnable.java
@@ -53,18 +53,20 @@
         int count = 0;
         int errors = 0;
         Stopwatch stopwatch = Stopwatch.createStarted();
+        Timestamp maxFetchedItemTs = Timestamp.valueOf(newLastIndexTs.toLocalDateTime());
         for (T c : fetchItems()) {
           try {
             Optional<Timestamp> itemTs = indexIfNeeded(c, newLastIndexTs);
             if (itemTs.isPresent()) {
               count++;
-              newLastIndexTs = maxTimestamp(newLastIndexTs, itemTs.get());
+              maxFetchedItemTs = maxTimestamp(maxFetchedItemTs, itemTs.get());
             }
           } catch (Exception e) {
             log.atSevere().withCause(e).log("Unable to reindex %s %s", itemNameString, c);
             errors++;
           }
         }
+        newLastIndexTs = maxTimestamp(newLastIndexTs, maxFetchedItemTs);
         long elapsedNanos = stopwatch.stop().elapsed(TimeUnit.NANOSECONDS);
         if (count > 0) {
           log.atInfo().log(
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
index a8ba722..252b7e8 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImpl.java
@@ -188,7 +188,7 @@
 
   private long getTsFromChangeAndDraftComments(ChangeNotes notes) {
     Change change = notes.getChange();
-    Timestamp changeTs = change.getLastUpdatedOn();
+    Timestamp changeTs = Timestamp.from(change.getLastUpdatedOn());
     for (HumanComment comment : commentsUtil.draftByChange(changeNotes.get())) {
       Timestamp commentTs = comment.writtenOn;
       changeTs = commentTs.after(changeTs) ? commentTs : changeTs;
diff --git a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
index ce47390..0e3fdc0 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProvider.java
@@ -16,6 +16,7 @@
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -157,6 +158,16 @@
     }
   }
 
+  @VisibleForTesting
+  void setChannel(JChannel channel) {
+    this.channel = channel;
+  }
+
+  @VisibleForTesting
+  void setPeerInfo(Optional<PeerInfo> peerInfo) {
+    this.peerInfo = peerInfo;
+  }
+
   @Override
   public Set<PeerInfo> get() {
     return peerInfo.isPresent() ? ImmutableSet.of(peerInfo.get()) : ImmutableSet.of();
@@ -178,4 +189,14 @@
     peerInfo = Optional.empty();
     peerAddress = null;
   }
+
+  @VisibleForTesting
+  Address getPeerAddress() {
+    return peerAddress;
+  }
+
+  @VisibleForTesting
+  void setPeerAddress(Address peerAddress) {
+    this.peerAddress = peerAddress;
+  }
 }
diff --git a/src/test/docker/gerrit/Dockerfile b/src/test/docker/gerrit/Dockerfile
index 4e64994..bbf9802 100644
--- a/src/test/docker/gerrit/Dockerfile
+++ b/src/test/docker/gerrit/Dockerfile
@@ -1,6 +1,6 @@
-FROM gerritcodereview/gerrit:3.5.0-rc2
+FROM gerritcodereview/gerrit:3.6.4
 
-ENV GERRIT_BRANCH=stable-3.5
+ENV GERRIT_BRANCH=stable-3.6
 
 ENV GERRIT_CI_URL=https://archive-ci.gerritforge.com/job
 
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnableTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnableTest.java
index 65001de..e338dcb 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnableTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/ChangeReindexRunnableTest.java
@@ -59,8 +59,10 @@
   @Mock private AllUsersName allUsers;
   @Mock private ChangeNotes.Factory changeNotesFactory;
   @Mock private Repository repo;
-  @Mock private ChangeNotesResult changeNotesRes;
-  @Mock private ChangeNotes changeNotes;
+  @Mock private ChangeNotesResult changeNotesResFirst;
+  @Mock private ChangeNotesResult changeNotesResSecond;
+  @Mock private ChangeNotes changeNotesFirst;
+  @Mock private ChangeNotes changeNotesSecond;
 
   private ChangeReindexRunnable changeReindexRunnable;
 
@@ -75,7 +77,7 @@
   public void changeIsIndexedWhenItIsCreatedAfterLastChangeReindex() throws Exception {
     Timestamp currentTime = Timestamp.valueOf(LocalDateTime.now());
     Timestamp afterCurrentTime = new Timestamp(currentTime.getTime() + 1000L);
-    Change change = newChange(afterCurrentTime);
+    Change change = newChange(123, afterCurrentTime);
 
     Optional<Timestamp> changeLastTs = changeReindexRunnable.indexIfNeeded(change, currentTime);
     assertThat(changeLastTs.isPresent()).isTrue();
@@ -88,16 +90,17 @@
     LocalDateTime currentTime = LocalDateTime.now(ZoneOffset.UTC);
     Timestamp afterCurrentTime =
         new Timestamp(currentTime.toEpochSecond(ZoneOffset.UTC) * 1000 + 1000L);
-    Change change = newChange(afterCurrentTime);
+    Change change = newChange(123, afterCurrentTime);
 
     when(indexTs.getUpdateTs(AbstractIndexRestApiServlet.IndexName.CHANGE))
         .thenReturn(Optional.of(currentTime));
     when(projectCache.all()).thenReturn(ImmutableSortedSet.of(change.getProject()));
     when(repoManager.openRepository(change.getProject())).thenReturn(repo);
-    when(changeNotesFactory.scan(repo, change.getProject())).thenReturn(Stream.of(changeNotesRes));
-    lenient().when(changeNotesRes.error()).thenReturn(Optional.empty());
-    when(changeNotesRes.notes()).thenReturn(changeNotes);
-    when(changeNotes.getChange()).thenReturn(change);
+    when(changeNotesFactory.scan(repo, change.getProject()))
+        .thenReturn(Stream.of(changeNotesResFirst));
+    lenient().when(changeNotesResFirst.error()).thenReturn(Optional.empty());
+    when(changeNotesResFirst.notes()).thenReturn(changeNotesFirst);
+    when(changeNotesFirst.getChange()).thenReturn(change);
 
     changeReindexRunnable.run();
 
@@ -109,7 +112,7 @@
     LocalDateTime currentTime = LocalDateTime.now(ZoneOffset.UTC);
     Timestamp afterCurrentTime =
         new Timestamp(currentTime.toEpochSecond(ZoneOffset.UTC) * 1000 + 1000L);
-    Change change = newChange(afterCurrentTime);
+    Change change = newChange(123, afterCurrentTime);
 
     when(indexTs.getUpdateTs(AbstractIndexRestApiServlet.IndexName.CHANGE))
         .thenReturn(Optional.of(currentTime));
@@ -119,10 +122,10 @@
     lenient().when(invalidChangeRes.notes()).thenThrow(IllegalStateException.class);
     when(invalidChangeRes.error()).thenReturn(Optional.of(new IllegalStateException()));
     when(changeNotesFactory.scan(repo, change.getProject()))
-        .thenReturn(Stream.of(invalidChangeRes, changeNotesRes));
-    when(changeNotesRes.error()).thenReturn(Optional.empty());
-    when(changeNotesRes.notes()).thenReturn(changeNotes);
-    when(changeNotes.getChange()).thenReturn(change);
+        .thenReturn(Stream.of(invalidChangeRes, changeNotesResFirst));
+    when(changeNotesResFirst.error()).thenReturn(Optional.empty());
+    when(changeNotesResFirst.notes()).thenReturn(changeNotesFirst);
+    when(changeNotesFirst.getChange()).thenReturn(change);
 
     changeReindexRunnable.run();
 
@@ -133,7 +136,7 @@
   public void groupIsNotIndexedWhenItIsCreatedBeforeLastGroupReindex() throws Exception {
     Timestamp currentTime = Timestamp.valueOf(LocalDateTime.now());
     Timestamp beforeCurrentTime = new Timestamp(currentTime.getTime() - 1000L);
-    Change change = newChange(beforeCurrentTime);
+    Change change = newChange(123, beforeCurrentTime);
 
     Optional<Timestamp> changeLastTs = changeReindexRunnable.indexIfNeeded(change, currentTime);
     assertThat(changeLastTs.isPresent()).isFalse();
@@ -141,16 +144,46 @@
         .index(changeProjectIndexKey(change), Operation.INDEX, Optional.empty());
   }
 
+  @Test
+  public void twoChangesAreIndexedDuringTheRunWhenItIsCreatedAfterLastChangeReindex()
+      throws Exception {
+    LocalDateTime currentTime = LocalDateTime.now(ZoneOffset.UTC);
+    Timestamp afterCurrentTime =
+        new Timestamp(currentTime.toEpochSecond(ZoneOffset.UTC) * 1000 + 1000L);
+    Timestamp secondAfterCurrentTime =
+        new Timestamp(currentTime.toEpochSecond(ZoneOffset.UTC) * 1000 + 2000L);
+    Change firstChange = newChange(123, secondAfterCurrentTime);
+    Change secondChange = newChange(456, afterCurrentTime);
+
+    when(indexTs.getUpdateTs(AbstractIndexRestApiServlet.IndexName.CHANGE))
+        .thenReturn(Optional.of(currentTime));
+    when(projectCache.all()).thenReturn(ImmutableSortedSet.of(firstChange.getProject()));
+    when(repoManager.openRepository(firstChange.getProject())).thenReturn(repo);
+    when(changeNotesFactory.scan(repo, firstChange.getProject()))
+        .thenReturn(Stream.of(changeNotesResFirst, changeNotesResSecond));
+    lenient().when(changeNotesResFirst.error()).thenReturn(Optional.empty());
+    lenient().when(changeNotesResSecond.error()).thenReturn(Optional.empty());
+    when(changeNotesResFirst.notes()).thenReturn(changeNotesFirst);
+    when(changeNotesResSecond.notes()).thenReturn(changeNotesSecond);
+    when(changeNotesFirst.getChange()).thenReturn(firstChange);
+    when(changeNotesSecond.getChange()).thenReturn(secondChange);
+
+    changeReindexRunnable.run();
+
+    verify(indexer).index(changeProjectIndexKey(firstChange), Operation.INDEX, Optional.empty());
+    verify(indexer).index(changeProjectIndexKey(secondChange), Operation.INDEX, Optional.empty());
+  }
+
   private String changeProjectIndexKey(Change change) {
     return change.getProject() + "~" + change.getChangeId();
   }
 
-  private Change newChange(Timestamp changeTs) {
+  private Change newChange(int id, Timestamp changeTs) {
     return new Change(
         Change.key("changekey"),
-        Change.id(123),
+        Change.id(id),
         Account.id(1000000),
         BranchNameKey.create("projectname", "main"),
-        changeTs);
+        changeTs.toInstant());
   }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnableTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnableTest.java
index afd6df4..0513d42 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnableTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/autoreindex/GroupReindexRunnableTest.java
@@ -110,7 +110,7 @@
             Collections.singletonList(
                 AccountGroupMemberAudit.builder()
                     .addedBy(Account.Id.tryParse("1").get())
-                    .addedOn(afterCurrentTime)
+                    .addedOn(afterCurrentTime.toInstant())
                     .memberId(Account.Id.tryParse("1").get())
                     .groupId(AccountGroup.Id.parse("1"))
                     .build()));
@@ -130,10 +130,10 @@
     AccountGroupMemberAudit accountGroupMemberAudit =
         AccountGroupMemberAudit.builder()
             .addedBy(Account.Id.tryParse("1").get())
-            .addedOn(beforeCurrentTime)
+            .addedOn(beforeCurrentTime.toInstant())
             .memberId(Account.Id.tryParse("1").get())
             .groupId(AccountGroup.Id.parse("2"))
-            .removed(Account.Id.tryParse("2").get(), afterCurrentTime)
+            .removed(Account.Id.tryParse("2").get(), afterCurrentTime.toInstant())
             .build();
     when(groups.getGroup(uuid)).thenReturn(getInternalGroup(currentTime));
     when(groups.getMembersAudit(any(), any()))
@@ -158,7 +158,7 @@
                     .groupId(AccountGroup.Id.parse("1"))
                     .includeUuid(UUID.parse("123"))
                     .addedBy(Account.Id.tryParse("1").get())
-                    .addedOn(afterCurrentTime)
+                    .addedOn(afterCurrentTime.toInstant())
                     .build()));
     Optional<Timestamp> groupLastTs =
         groupReindexRunnable.indexIfNeeded(groupReference, currentTime);
@@ -178,8 +178,8 @@
             .groupId(AccountGroup.Id.parse("1"))
             .includeUuid(UUID.parse("123"))
             .addedBy(Account.Id.tryParse("1").get())
-            .addedOn(beforeCurrentTime)
-            .removed(Account.Id.tryParse("2").get(), afterCurrentTime)
+            .addedOn(beforeCurrentTime.toInstant())
+            .removed(Account.Id.tryParse("2").get(), afterCurrentTime.toInstant())
             .build();
 
     when(groups.getGroup(uuid)).thenReturn(getInternalGroup(beforeCurrentTime));
@@ -201,7 +201,7 @@
             .setOwnerGroupUUID(uuid)
             .setVisibleToAll(true)
             .setGroupUUID(UUID.parse("12"))
-            .setCreatedOn(timestamp)
+            .setCreatedOn(timestamp.toInstant())
             .setMembers(ImmutableSet.<Id>builder().build())
             .setSubgroups(ImmutableSet.<UUID>builder().build())
             .build());
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
index 7c1e6df..ba0c8c5 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/forwarder/ForwardedIndexChangeHandlerTest.java
@@ -30,8 +30,8 @@
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.util.OneOffRequestContext;
-import com.google.gerrit.server.util.time.TimeUtil;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.concurrent.ScheduledExecutorService;
 import org.junit.Before;
@@ -67,7 +67,7 @@
   @Before
   public void setUp() throws Exception {
     id = Change.id(TEST_CHANGE_NUMBER);
-    Change change = new Change(null, id, null, null, TimeUtil.nowTs());
+    Change change = new Change(null, id, null, null, Instant.now());
     when(changeNotes.getChange()).thenReturn(change);
     when(configMock.index()).thenReturn(indexMock);
     when(changeCheckerFactoryMock.create(any())).thenReturn(changeCheckerAbsentMock);
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImplTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImplTest.java
new file mode 100644
index 0000000..38f71e5
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/ChangeCheckerImplTest.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2022 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.ericsson.gerrit.plugins.highavailability.index;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.highavailability.forwarder.IndexEvent;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ChangeCheckerImplTest {
+
+  @Mock private GitRepositoryManager gitRepoMgr;
+  @Mock private CommentsUtil commentsUtil;
+  @Mock private ChangeFinder changeFinder;
+  @Mock private OneOffRequestContext oneOffReqCtx;
+  @Mock private ChangeNotes testChangeNotes;
+  @Mock private Change testChange;
+
+  private final Instant testLastUpdatedOn = Instant.now();
+  private final String changeId = "1";
+  Optional<IndexEvent> event = Optional.empty();
+  private Optional<Long> computedChangeTs = Optional.empty();
+  private ChangeCheckerImpl changeChecker;
+
+  @Before
+  public void setUp() {
+    changeChecker =
+        new ChangeCheckerImpl(gitRepoMgr, commentsUtil, changeFinder, oneOffReqCtx, changeId);
+  }
+
+  @Test
+  public void testGetChangeNotes() {
+    when(changeFinder.findOne(changeId)).thenReturn(Optional.of(testChangeNotes));
+    assertThat(changeChecker.getChangeNotes()).isEqualTo(Optional.of(testChangeNotes));
+  }
+
+  @Test
+  public void testGetComputedChangeTs() {
+    long testTime = Timestamp.from(testLastUpdatedOn).getTime();
+    computedChangeTs = Optional.of(testTime / 1000);
+    when(changeChecker.getChangeNotes()).thenReturn(Optional.of(testChangeNotes));
+    when(testChangeNotes.getChange()).thenReturn(testChange);
+    when(testChange.getLastUpdatedOn()).thenReturn(testLastUpdatedOn);
+    assertThat(changeChecker.getComputedChangeTs()).isEqualTo(computedChangeTs);
+  }
+
+  @Test
+  public void testNewIndexEventWhenChangeTimeStampIsEmpty() throws IOException {
+    assertThat(changeChecker.newIndexEvent().isPresent()).isFalse();
+  }
+
+  @Test
+  public void testIsChangeUpToDateWhenComputedChangeTsIsNotPresent() throws IOException {
+    assertThat(changeChecker.isChangeUpToDate(event)).isFalse();
+  }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandlerTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandlerTest.java
index 4793678..7ed5f55 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandlerTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/index/IndexEventHandlerTest.java
@@ -22,7 +22,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
@@ -78,6 +78,7 @@
   private static final String OTHER_UUID = "4";
   private static final Integer INDEX_WAIT_TIMEOUT_MS = 5;
   private static final int MAX_TEST_PARALLELISM = 4;
+  private static final String EXECUTOR_THREAD_NAME = "EXECUTOR_THREAD";
 
   private IndexEventHandler indexEventHandler;
   @Mock private Forwarder forwarder;
@@ -93,6 +94,7 @@
   @Mock private RequestContext mockCtx;
   @Mock private Configuration configuration;
   private IndexEventLocks idLocks;
+  private Thread executorThread;
 
   private CurrentRequestContext currCtx =
       new CurrentRequestContext(null, null, null) {
@@ -157,6 +159,7 @@
   @Test
   public void shouldIndexInRemoteOnChangeIndexedEvent() throws Exception {
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
+    executorThread.join();
     verify(forwarder).indexChange(eq(PROJECT_NAME), eq(CHANGE_ID), any());
   }
 
@@ -259,7 +262,7 @@
     setUpIndexEventHandler(currCtx, locks);
 
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
-
+    executorThread.join();
     verify(locks, times(2)).withLock(any(), any(), any());
     verify(forwarder, times(1)).indexChange(eq(PROJECT_NAME), eq(CHANGE_ID), any());
   }
@@ -278,9 +281,8 @@
     when(httpCfg.maxTries()).thenReturn(10);
     when(cfg.http()).thenReturn(httpCfg);
     setUpIndexEventHandler(currCtx, locks, cfg);
-
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
-
+    executorThread.join();
     verify(locks, times(11)).withLock(any(), any(), any());
     verify(forwarder, never()).indexChange(eq(PROJECT_NAME), eq(CHANGE_ID), any());
   }
@@ -301,7 +303,7 @@
     setUpIndexEventHandler(currCtx, locks, cfg);
 
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
-
+    executorThread.join();
     verify(locks, times(1)).withLock(any(), any(), any());
     verify(forwarder, never()).indexChange(eq(PROJECT_NAME), eq(CHANGE_ID), any());
   }
@@ -321,8 +323,9 @@
     setUpIndexEventHandler(currCtx, locks);
 
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
+    executorThread.join();
     indexEventHandler.onAccountIndexed(accountId.get());
-
+    executorThread.join();
     verify(forwarder, never()).indexChange(eq(PROJECT_NAME), eq(CHANGE_ID), any());
     verify(forwarder).indexAccount(eq(ACCOUNT_ID), any());
   }
@@ -335,29 +338,32 @@
     Configuration.Index cfgIndex = mock(Configuration.Index.class);
     when(cfgMock.index()).thenReturn(cfgIndex);
     when(cfgIndex.synchronizeForced()).thenReturn(true);
-
     setUpIndexEventHandler(new CurrentRequestContext(threadLocalCtxMock, cfgMock, oneOffCtxMock));
+
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
+    executorThread.join();
     verify(forwarder).indexChange(eq(PROJECT_NAME), eq(CHANGE_ID), any());
   }
 
   @Test
   public void shouldIndexInRemoteOnAccountIndexedEvent() throws Exception {
     indexEventHandler.onAccountIndexed(accountId.get());
+    executorThread.join();
     verify(forwarder).indexAccount(eq(ACCOUNT_ID), any());
   }
 
   @Test
   public void shouldDeleteFromIndexInRemoteOnChangeDeletedEvent() throws Exception {
     indexEventHandler.onChangeDeleted(changeId.get());
+    executorThread.join();
     verify(forwarder).deleteChangeFromIndex(eq(CHANGE_ID), any());
-    verifyZeroInteractions(
-        changeCheckerMock); // Deleted changes should not be checked against NoteDb
+    verifyNoInteractions(changeCheckerMock); // Deleted changes should not be checked against NoteDb
   }
 
   @Test
   public void shouldIndexInRemoteOnGroupIndexedEvent() throws Exception {
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
+    executorThread.join();
     verify(forwarder).indexGroup(eq(UUID), any());
   }
 
@@ -367,7 +373,7 @@
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
     indexEventHandler.onChangeDeleted(changeId.get());
     Context.unsetForwardedEvent();
-    verifyZeroInteractions(forwarder);
+    verifyNoInteractions(forwarder);
   }
 
   @Test
@@ -376,7 +382,7 @@
     indexEventHandler.onAccountIndexed(accountId.get());
     indexEventHandler.onAccountIndexed(accountId.get());
     Context.unsetForwardedEvent();
-    verifyZeroInteractions(forwarder);
+    verifyNoInteractions(forwarder);
   }
 
   @Test
@@ -385,7 +391,7 @@
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
     Context.unsetForwardedEvent();
-    verifyZeroInteractions(forwarder);
+    verifyNoInteractions(forwarder);
   }
 
   @Test
@@ -686,7 +692,6 @@
   }
 
   private class CurrentThreadScheduledExecutorService implements ScheduledExecutorService {
-
     @Override
     public void shutdown() {}
 
@@ -752,7 +757,9 @@
 
     @Override
     public void execute(Runnable command) {
-      command.run();
+      executorThread = new Thread(command);
+      executorThread.setName(EXECUTOR_THREAD_NAME);
+      executorThread.start();
     }
 
     @Override
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinderTest.java
index bfcffbb..c375345 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinderTest.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/InetAddressFinderTest.java
@@ -16,10 +16,20 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.mockito.Answers.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.ericsson.gerrit.plugins.highavailability.Configuration;
 import com.google.common.collect.ImmutableList;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Optional;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -32,11 +42,68 @@
   @Mock(answer = RETURNS_DEEP_STUBS)
   private Configuration configuration;
 
+  @Mock private NetworkInterface mockInterface;
+
   private InetAddressFinder finder;
+  private List<NetworkInterface> testNetworkInterfaces;
 
   @Before
   public void setUp() {
     finder = new InetAddressFinder(configuration);
+    testNetworkInterfaces = new ArrayList<>();
+  }
+
+  @Test
+  public void testNoSuitableInterfaceWhenFindFirstAppropriateAddress() throws SocketException {
+    when(mockInterface.isLoopback()).thenReturn(true);
+    when(configuration.jgroups().skipInterface()).thenReturn(ImmutableList.of("mockInterface1"));
+    testNetworkInterfaces.add(mockInterface);
+    assertThat(finder.findFirstAppropriateAddress(testNetworkInterfaces).isPresent()).isFalse();
+  }
+
+  @Test
+  public void testOptionalEmptyIsReturnedWhenFindFirstAppropriateAddress() throws SocketException {
+    setUpCustomMockInterfaceMocks();
+    when(configuration.jgroups().skipInterface()).thenReturn(ImmutableList.of());
+    testNetworkInterfaces.add(mockInterface);
+    Enumeration mockInetAddresses = mock(Enumeration.class);
+
+    when(mockInterface.getInetAddresses()).thenReturn(mockInetAddresses);
+    assertThat(finder.findFirstAppropriateAddress(testNetworkInterfaces))
+        .isEqualTo(Optional.empty());
+  }
+
+  @Test
+  public void testInet6AddressIsReturnedWhenFindFirstAppropriateAddress() throws SocketException {
+    setUpCustomMockInterfaceMocks();
+    when(configuration.jgroups().skipInterface()).thenReturn(ImmutableList.of());
+    testNetworkInterfaces.add(mockInterface);
+    Inet6Address mockInet6Address = mock(Inet6Address.class);
+    List<Inet6Address> mocklist = new ArrayList<>();
+    mocklist.add(mockInet6Address);
+    Enumeration mockInetAddresses = Collections.enumeration(mocklist);
+
+    when(mockInterface.getInetAddresses()).thenReturn(mockInetAddresses);
+    assertThat(finder.findFirstAppropriateAddress(testNetworkInterfaces))
+        .isEqualTo(Optional.of(mockInet6Address));
+  }
+
+  @Test
+  public void testInet4AddressIsReturnedWhenFindFirstAppropriateAddress() throws SocketException {
+    setUpCustomMockInterfaceMocks();
+    when(configuration.jgroups().skipInterface()).thenReturn(ImmutableList.of());
+    System.setProperty("java.net.preferIPv4Stack", "true");
+    testNetworkInterfaces.add(mockInterface);
+    Inet4Address mockInet4Address = mock(Inet4Address.class);
+    List<Inet4Address> mocklist = new ArrayList<>();
+    mocklist.add(mockInet4Address);
+
+    Enumeration mockInetAddresses = Collections.enumeration(mocklist);
+    when(mockInterface.getInetAddresses()).thenReturn(mockInetAddresses);
+
+    finder = new InetAddressFinder(configuration);
+    assertThat(finder.findFirstAppropriateAddress(testNetworkInterfaces))
+        .isEqualTo(Optional.of(mockInet4Address));
   }
 
   @Test
@@ -64,4 +131,10 @@
     assertThat(finder.shouldSkip("foo1")).isTrue();
     assertThat(finder.shouldSkip("bar")).isFalse();
   }
+
+  private void setUpCustomMockInterfaceMocks() throws SocketException {
+    when(mockInterface.isLoopback()).thenReturn(false);
+    when(mockInterface.isUp()).thenReturn(true);
+    when(mockInterface.supportsMulticast()).thenReturn(true);
+  }
 }
diff --git a/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProviderTest.java b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProviderTest.java
new file mode 100644
index 0000000..1110fc3
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/highavailability/peers/jgroups/JGroupsPeerInfoProviderTest.java
@@ -0,0 +1,158 @@
+// Copyright (C) 2022 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.ericsson.gerrit.plugins.highavailability.peers.jgroups;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Answers.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.highavailability.Configuration;
+import com.ericsson.gerrit.plugins.highavailability.peers.PeerInfo;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.jgroups.Address;
+import org.jgroups.JChannel;
+import org.jgroups.Message;
+import org.jgroups.View;
+import org.jgroups.stack.IpAddress;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.Test.None;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class JGroupsPeerInfoProviderTest {
+
+  @Mock(answer = RETURNS_DEEP_STUBS)
+  private Configuration pluginConfigurationMock;
+
+  private InetAddressFinder finder;
+  private JGroupsPeerInfoProvider jGroupsPeerInfoProvider;
+  private Optional<PeerInfo> peerInfo;
+  @Mock private JChannel channel;
+  @Mock private MyUrlProvider myUrlProviderTest;
+  @Mock private Message message;
+  @Mock private Address peerAddress;
+  @Mock private View view;
+  @Mock private List<Address> members;
+
+  @Before
+  public void setUp() throws Exception {
+    finder = new InetAddressFinder(pluginConfigurationMock);
+    jGroupsPeerInfoProvider =
+        new JGroupsPeerInfoProvider(pluginConfigurationMock, finder, myUrlProviderTest);
+    peerInfo = Optional.of(new PeerInfo("test message"));
+    channel.setName("testChannel");
+  }
+
+  @Test
+  public void testRecieveWhenPeerAddressIsNull() {
+    when(message.getSrc()).thenReturn(peerAddress);
+    when(message.getObject()).thenReturn("test message");
+
+    jGroupsPeerInfoProvider.receive(message);
+
+    assertThat(jGroupsPeerInfoProvider.getPeerAddress()).isEqualTo(peerAddress);
+    Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
+    for (PeerInfo testPeerInfo : testPeerInfoSet) {
+      assertThat(testPeerInfo.getDirectUrl()).contains("test message");
+    }
+    assertThat(testPeerInfoSet.size()).isEqualTo(1);
+  }
+
+  @Test
+  public void testReceiveWhenPeerAddressIsNotNull() throws Exception {
+    jGroupsPeerInfoProvider.setPeerAddress(new IpAddress("checkAddress.com"));
+
+    jGroupsPeerInfoProvider.receive(message);
+
+    Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
+    assertThat(testPeerInfoSet.isEmpty()).isTrue();
+    assertThat(testPeerInfoSet.size()).isEqualTo(0);
+  }
+
+  @Test(expected = None.class)
+  public void testViewAcceptedWithNoExceptionThrown() throws Exception {
+    when(view.getMembers()).thenReturn(members);
+    when(view.size()).thenReturn(3);
+    when(members.size()).thenReturn(3);
+    jGroupsPeerInfoProvider.setChannel(channel);
+    jGroupsPeerInfoProvider.viewAccepted(view);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testViewAcceptedWithExceptionThrown() throws Exception {
+    when(view.getMembers()).thenReturn(members);
+    when(view.size()).thenReturn(2);
+    when(members.size()).thenReturn(2);
+    jGroupsPeerInfoProvider.viewAccepted(view);
+  }
+
+  @Test
+  public void testViewAcceptedWhenPeerAddressIsNotNullAndIsNotMemberOfView() {
+    when(view.getMembers()).thenReturn(members);
+    when(view.size()).thenReturn(2);
+    when(members.size()).thenReturn(2);
+    when(members.contains(peerAddress)).thenReturn(false);
+    jGroupsPeerInfoProvider.setPeerAddress(peerAddress);
+    jGroupsPeerInfoProvider.setPeerInfo(peerInfo);
+    jGroupsPeerInfoProvider.setChannel(channel);
+    jGroupsPeerInfoProvider.viewAccepted(view);
+
+    assertThat(jGroupsPeerInfoProvider.getPeerAddress()).isEqualTo(null);
+    Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
+    assertThat(testPeerInfoSet.isEmpty()).isTrue();
+    assertThat(testPeerInfoSet.size()).isEqualTo(0);
+  }
+
+  @Test
+  public void testConnect() throws NoSuchFieldException, IllegalAccessException {
+    jGroupsPeerInfoProvider.connect();
+    Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
+    assertThat(testPeerInfoSet.isEmpty()).isTrue();
+    assertThat(testPeerInfoSet.size()).isEqualTo(0);
+  }
+
+  @Test
+  public void testGetWhenPeerInfoIsOptionalEmpty() {
+    Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
+    assertThat(testPeerInfoSet.isEmpty()).isTrue();
+    assertThat(testPeerInfoSet.size()).isEqualTo(0);
+  }
+
+  @Test
+  public void testGetWhenPeerInfoIsPresent() {
+    jGroupsPeerInfoProvider.setPeerInfo(peerInfo);
+    Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
+    for (PeerInfo testPeerInfo : testPeerInfoSet) {
+      assertThat(testPeerInfo.getDirectUrl()).contains("test message");
+    }
+    assertThat(testPeerInfoSet.size()).isEqualTo(1);
+  }
+
+  @Test
+  public void testStop() throws Exception {
+    jGroupsPeerInfoProvider.setPeerAddress(peerAddress);
+    jGroupsPeerInfoProvider.setPeerInfo(peerInfo);
+    jGroupsPeerInfoProvider.stop();
+    assertThat(jGroupsPeerInfoProvider.getPeerAddress()).isEqualTo(null);
+    Set<PeerInfo> testPeerInfoSet = jGroupsPeerInfoProvider.get();
+    assertThat(testPeerInfoSet.isEmpty()).isTrue();
+    assertThat(testPeerInfoSet.size()).isEqualTo(0);
+  }
+}
diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000..1f0955d
--- /dev/null
+++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline