Merge branch 'stable-3.5' into stable-3.6

* stable-3.5:
  Bump global-refdb to v3.5.4
  Do not include global-refdb library in high-availability
  Bump global-refdb to v3.4.8
  Fix issue with incorrect import for Nullable annotation
  Cache the resolution of allowed listeners
  Bump Gerrit to v3.4.5
  Allow unrestricted listeners to be called for forwarded events

Change-Id: I5f55c12d22194a191a2c087c88210c502c831401
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/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 fcd345d..c0b60c3 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 62d9c10..edc11b1 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;
@@ -138,7 +139,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 1a2ebf7..67f5c0b 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
@@ -177,7 +177,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/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 578dc17..81f16e4 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;
@@ -76,6 +76,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;
@@ -91,6 +92,7 @@
   @Mock private RequestContext mockCtx;
   @Mock private Configuration configuration;
   private IndexEventLocks idLocks;
+  private Thread executorThread;
 
   private CurrentRequestContext currCtx =
       new CurrentRequestContext(null, null, null) {
@@ -155,6 +157,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());
   }
 
@@ -257,7 +260,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());
   }
@@ -276,9 +279,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());
   }
@@ -299,7 +301,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());
   }
@@ -319,8 +321,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());
   }
@@ -333,29 +336,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());
   }
 
@@ -365,7 +371,7 @@
     indexEventHandler.onChangeIndexed(PROJECT_NAME, changeId.get());
     indexEventHandler.onChangeDeleted(changeId.get());
     Context.unsetForwardedEvent();
-    verifyZeroInteractions(forwarder);
+    verifyNoInteractions(forwarder);
   }
 
   @Test
@@ -374,7 +380,7 @@
     indexEventHandler.onAccountIndexed(accountId.get());
     indexEventHandler.onAccountIndexed(accountId.get());
     Context.unsetForwardedEvent();
-    verifyZeroInteractions(forwarder);
+    verifyNoInteractions(forwarder);
   }
 
   @Test
@@ -383,7 +389,7 @@
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
     indexEventHandler.onGroupIndexed(accountGroupUUID.get());
     Context.unsetForwardedEvent();
-    verifyZeroInteractions(forwarder);
+    verifyNoInteractions(forwarder);
   }
 
   @Test
@@ -684,7 +690,6 @@
   }
 
   private class CurrentThreadScheduledExecutorService implements ScheduledExecutorService {
-
     @Override
     public void shutdown() {}
 
@@ -750,7 +755,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