Add 8.9.* to supported versions

Also, run tests with 8.9.2 by refactoring most of the code into abstract
classes. The tests need "action.destructive_requires_name" set to false
in order to close indexes with a wildcard. False is the default in
Elasticsearch 7.*, but not in 8.*.

Change-Id: I42bd5efb57004ad05c21c21628dbd13031b3c686
diff --git a/BUILD b/BUILD
index 71977a6..0f6f594 100644
--- a/BUILD
+++ b/BUILD
@@ -62,6 +62,20 @@
 
 SUFFIX = "sTest.java"
 
+ABSTRACT_ELASTICSEARCH_TESTS = {i: "Elastic*Query" + i.capitalize() + SUFFIX for i in TYPES}
+
+[java_library(
+    name = "abstract_elasticsearch_query_%ss_test" % name,
+    testonly = True,
+    srcs = glob(["src/test/java/com/google/gerrit/elasticsearch/" + src]),
+    visibility = ["//visibility:public"],
+    deps = ELASTICSEARCH_DEPS + PLUGIN_TEST_DEPS + [
+        QUERY_TESTS_DEP % name,
+        ":elasticsearch_test_utils",
+        ":index-elasticsearch__plugin",
+    ],
+) for name, src in ABSTRACT_ELASTICSEARCH_TESTS.items()]
+
 ELASTICSEARCH_TESTS_V7 = {i: "ElasticV7Query" + i.capitalize() + SUFFIX for i in TYPES}
 
 [junit_tests(
@@ -77,9 +91,29 @@
         QUERY_TESTS_DEP % name,
         ":elasticsearch_test_utils",
         ":index-elasticsearch__plugin",
+        ":abstract_elasticsearch_query_%ss_test" % name,
     ],
 ) for name, src in ELASTICSEARCH_TESTS_V7.items()]
 
+ELASTICSEARCH_TESTS_V8 = {i: "ElasticV8Query" + i.capitalize() + SUFFIX for i in TYPES}
+
+[junit_tests(
+    name = "elasticsearch_query_%ss_test_V8" % name,
+    size = "enormous",
+    srcs = ["src/test/java/com/google/gerrit/elasticsearch/" + src],
+    tags = [
+        "docker",
+        "elastic",
+        "exclusive",
+    ],
+    deps = ELASTICSEARCH_DEPS + PLUGIN_TEST_DEPS + [
+        QUERY_TESTS_DEP % name,
+        ":elasticsearch_test_utils",
+        ":index-elasticsearch__plugin",
+        ":abstract_elasticsearch_query_%ss_test" % name,
+    ],
+) for name, src in ELASTICSEARCH_TESTS_V8.items()]
+
 junit_tests(
     name = "index-elasticsearch_tests",
     size = "small",
diff --git a/src/main/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/src/main/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index dffdf3e..2fad786 100644
--- a/src/main/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/src/main/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -18,7 +18,8 @@
 import java.util.regex.Pattern;
 
 public enum ElasticVersion {
-  V7_16("7.16.*");
+  V7_16("7.16.*"),
+  V8_9("8.9.*");
 
   private final String version;
   private final Pattern pattern;
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryAccountsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryAccountsTest.java
new file mode 100644
index 0000000..ce16b2e
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryAccountsTest.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Injector;
+import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.Test;
+
+public abstract class ElasticAbstractQueryAccountsTest extends AbstractQueryAccountsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return ElasticTestUtils.createConfig();
+  }
+
+  @ConfigSuite.Config
+  public static Config searchAfterPaginationType() {
+    Config config = defaultConfig();
+    config.setString("index", null, "paginationType", "SEARCH_AFTER");
+    return config;
+  }
+
+  private static ElasticContainer container;
+  private static CloseableHttpAsyncClient client;
+
+  protected static void startIndexService(ElasticVersion elasticVersion) {
+    container = ElasticContainer.createAndStart(elasticVersion);
+    client = ElasticTestUtils.createHttpAsyncClient(container);
+    client.start();
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    return ElasticTestUtils.createInjector(config, testName, container);
+  }
+
+  @Test
+  public void testErrorResponseFromAccountIndex() throws Exception {
+    gApi.accounts().self().index();
+
+    ElasticTestUtils.closeIndex(client, container, testName);
+    StorageException thrown =
+        assertThrows(StorageException.class, () -> gApi.accounts().self().index());
+    assertThat(thrown).hasMessageThat().contains("Failed to replace account");
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryChangesTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryChangesTest.java
new file mode 100644
index 0000000..d37e626
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryChangesTest.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.GerritTestName;
+import com.google.gerrit.testing.InMemoryRepositoryManager;
+import com.google.inject.Injector;
+import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Rule;
+import org.junit.Test;
+
+public abstract class ElasticAbstractQueryChangesTest extends AbstractQueryChangesTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return ElasticTestUtils.createConfig();
+  }
+
+  @ConfigSuite.Config
+  public static Config searchAfterPaginationType() {
+    Config config = defaultConfig();
+    config.setString("index", null, "paginationType", "SEARCH_AFTER");
+    return config;
+  }
+
+  private static ElasticContainer container;
+  private static CloseableHttpAsyncClient client;
+
+  protected static void startIndexService(ElasticVersion elasticVersion) {
+    container = ElasticContainer.createAndStart(elasticVersion);
+    client = ElasticTestUtils.createHttpAsyncClient(container);
+    client.start();
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  @Rule public final GerritTestName testName = new GerritTestName();
+
+  @After
+  public void closeIndex() throws Exception {
+    // Close the index after each test to prevent exceeding Elasticsearch's
+    // shard limit (see Issue 10120).
+    ElasticTestUtils.closeIndex(client, container, testName);
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    return ElasticTestUtils.createInjector(config, testName, container);
+  }
+
+  @Test
+  public void testErrorResponseFromChangeIndex() throws Exception {
+    TestRepository<InMemoryRepositoryManager.Repo> repo = createProject("repo");
+    Change c = insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
+    gApi.changes().id(c.getChangeId()).index();
+
+    ElasticTestUtils.closeIndex(client, container, testName);
+    StorageException thrown =
+        assertThrows(StorageException.class, () -> gApi.changes().id(c.getChangeId()).index());
+    assertThat(thrown).hasMessageThat().contains("Failed to reindex change");
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryGroupsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryGroupsTest.java
new file mode 100644
index 0000000..f449bca
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryGroupsTest.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.groups.GroupApi;
+import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Injector;
+import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.Test;
+
+public abstract class ElasticAbstractQueryGroupsTest extends AbstractQueryGroupsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return ElasticTestUtils.createConfig();
+  }
+
+  @ConfigSuite.Config
+  public static Config searchAfterPaginationType() {
+    Config config = defaultConfig();
+    config.setString("index", null, "paginationType", "SEARCH_AFTER");
+    return config;
+  }
+
+  private static ElasticContainer container;
+  private static CloseableHttpAsyncClient client;
+
+  protected static void startIndexService(ElasticVersion elasticVersion) {
+    container = ElasticContainer.createAndStart(elasticVersion);
+    client = ElasticTestUtils.createHttpAsyncClient(container);
+    client.start();
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    return ElasticTestUtils.createInjector(config, testName, container);
+  }
+
+  @Test
+  public void testErrorResponseFromGroupIndex() throws Exception {
+    GroupApi group = gApi.groups().create("test");
+    group.index();
+
+    ElasticTestUtils.closeIndex(client, container, testName);
+    StorageException thrown = assertThrows(StorageException.class, () -> group.index());
+    assertThat(thrown).hasMessageThat().contains("Failed to replace group");
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryProjectsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryProjectsTest.java
new file mode 100644
index 0000000..5f4847c
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticAbstractQueryProjectsTest.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.projects.ProjectApi;
+import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Injector;
+import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
+import org.eclipse.jgit.lib.Config;
+import org.junit.AfterClass;
+import org.junit.Test;
+
+public abstract class ElasticAbstractQueryProjectsTest extends AbstractQueryProjectsTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return ElasticTestUtils.createConfig();
+  }
+
+  @ConfigSuite.Config
+  public static Config searchAfterPaginationType() {
+    Config config = defaultConfig();
+    config.setString("index", null, "paginationType", "SEARCH_AFTER");
+    return config;
+  }
+
+  private static ElasticContainer container;
+  private static CloseableHttpAsyncClient client;
+
+  protected static void startIndexService(ElasticVersion elasticVersion) {
+    container = ElasticContainer.createAndStart(elasticVersion);
+    client = ElasticTestUtils.createHttpAsyncClient(container);
+    client.start();
+  }
+
+  @AfterClass
+  public static void stopElasticsearchServer() {
+    if (container != null) {
+      container.stop();
+    }
+  }
+
+  @Override
+  protected void initAfterLifecycleStart() throws Exception {
+    super.initAfterLifecycleStart();
+    ElasticTestUtils.createAllIndexes(injector);
+  }
+
+  @Override
+  protected Injector createInjector() {
+    return ElasticTestUtils.createInjector(config, testName, container);
+  }
+
+  @Test
+  public void testErrorResponseFromProjectIndex() throws Exception {
+    ProjectApi project = gApi.projects().create("test");
+    project.index(false);
+
+    ElasticTestUtils.closeIndex(client, container, testName);
+    StorageException thrown = assertThrows(StorageException.class, () -> project.index(false));
+    assertThat(thrown).hasMessageThat().contains("Failed to replace project");
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java
index b392f40..af64c7e 100644
--- a/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -108,12 +108,15 @@
     switch (version) {
       case V7_16:
         return image.withTag("7.16.2");
+      case V8_9:
+        return image.withTag("8.9.2");
     }
     throw new IllegalStateException("No tests for version: " + version.name());
   }
 
   private ElasticContainer(ElasticVersion version) {
     super(getImageName(version));
+    withEnv("action.destructive_requires_name", "false");
   }
 
   @Override
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
index 8eec618..a9c8361 100644
--- a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryAccountsTest.java
@@ -14,67 +14,11 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.inject.Injector;
-import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
 import org.junit.BeforeClass;
-import org.junit.Test;
 
-public class ElasticV7QueryAccountsTest extends AbstractQueryAccountsTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return ElasticTestUtils.createConfig();
-  }
-
-  @ConfigSuite.Config
-  public static Config searchAfterPaginationType() {
-    Config config = defaultConfig();
-    config.setString("index", null, "paginationType", "SEARCH_AFTER");
-    return config;
-  }
-
-  private static ElasticContainer container;
-  private static CloseableHttpAsyncClient client;
-
+public class ElasticV7QueryAccountsTest extends ElasticAbstractQueryAccountsTest {
   @BeforeClass
   public static void startIndexService() {
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_16);
-    client = ElasticTestUtils.createHttpAsyncClient(container);
-    client.start();
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    return ElasticTestUtils.createInjector(config, testName, container);
-  }
-
-  @Test
-  public void testErrorResponseFromAccountIndex() throws Exception {
-    gApi.accounts().self().index();
-
-    ElasticTestUtils.closeIndex(client, container, testName);
-    StorageException thrown =
-        assertThrows(StorageException.class, () -> gApi.accounts().self().index());
-    assertThat(thrown).hasMessageThat().contains("Failed to replace account");
+    startIndexService(ElasticVersion.V7_16);
   }
 }
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
index c6cbd99..9961987 100644
--- a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryChangesTest.java
@@ -14,84 +14,11 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.gerrit.testing.GerritTestName;
-import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.inject.Injector;
-import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Config;
-import org.junit.After;
-import org.junit.AfterClass;
 import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
 
-public class ElasticV7QueryChangesTest extends AbstractQueryChangesTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return ElasticTestUtils.createConfig();
-  }
-
-  @ConfigSuite.Config
-  public static Config searchAfterPaginationType() {
-    Config config = defaultConfig();
-    config.setString("index", null, "paginationType", "SEARCH_AFTER");
-    return config;
-  }
-
-  private static ElasticContainer container;
-  private static CloseableHttpAsyncClient client;
-
+public class ElasticV7QueryChangesTest extends ElasticAbstractQueryChangesTest {
   @BeforeClass
   public static void startIndexService() {
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_16);
-    client = ElasticTestUtils.createHttpAsyncClient(container);
-    client.start();
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Rule public final GerritTestName testName = new GerritTestName();
-
-  @After
-  public void closeIndex() throws Exception {
-    // Close the index after each test to prevent exceeding Elasticsearch's
-    // shard limit (see Issue 10120).
-    ElasticTestUtils.closeIndex(client, container, testName);
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    return ElasticTestUtils.createInjector(config, testName, container);
-  }
-
-  @Test
-  public void testErrorResponseFromChangeIndex() throws Exception {
-    TestRepository<InMemoryRepositoryManager.Repo> repo = createProject("repo");
-    Change c = insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
-    gApi.changes().id(c.getChangeId()).index();
-
-    ElasticTestUtils.closeIndex(client, container, testName);
-    StorageException thrown =
-        assertThrows(StorageException.class, () -> gApi.changes().id(c.getChangeId()).index());
-    assertThat(thrown).hasMessageThat().contains("Failed to reindex change");
+    startIndexService(ElasticVersion.V7_16);
   }
 }
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
index 20a88e9..9a48181 100644
--- a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryGroupsTest.java
@@ -14,68 +14,11 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.api.groups.GroupApi;
-import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.inject.Injector;
-import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
 import org.junit.BeforeClass;
-import org.junit.Test;
 
-public class ElasticV7QueryGroupsTest extends AbstractQueryGroupsTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return ElasticTestUtils.createConfig();
-  }
-
-  @ConfigSuite.Config
-  public static Config searchAfterPaginationType() {
-    Config config = defaultConfig();
-    config.setString("index", null, "paginationType", "SEARCH_AFTER");
-    return config;
-  }
-
-  private static ElasticContainer container;
-  private static CloseableHttpAsyncClient client;
-
+public class ElasticV7QueryGroupsTest extends ElasticAbstractQueryGroupsTest {
   @BeforeClass
   public static void startIndexService() {
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_16);
-    client = ElasticTestUtils.createHttpAsyncClient(container);
-    client.start();
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    return ElasticTestUtils.createInjector(config, testName, container);
-  }
-
-  @Test
-  public void testErrorResponseFromGroupIndex() throws Exception {
-    GroupApi group = gApi.groups().create("test");
-    group.index();
-
-    ElasticTestUtils.closeIndex(client, container, testName);
-    StorageException thrown = assertThrows(StorageException.class, () -> group.index());
-    assertThat(thrown).hasMessageThat().contains("Failed to replace group");
+    startIndexService(ElasticVersion.V7_16);
   }
 }
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
index 6d7ce80..f37bbff 100644
--- a/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV7QueryProjectsTest.java
@@ -14,68 +14,11 @@
 
 package com.google.gerrit.elasticsearch;
 
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.testing.GerritJUnit.assertThrows;
-
-import com.google.gerrit.exceptions.StorageException;
-import com.google.gerrit.extensions.api.projects.ProjectApi;
-import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
-import com.google.gerrit.testing.ConfigSuite;
-import com.google.inject.Injector;
-import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
-import org.eclipse.jgit.lib.Config;
-import org.junit.AfterClass;
 import org.junit.BeforeClass;
-import org.junit.Test;
 
-public class ElasticV7QueryProjectsTest extends AbstractQueryProjectsTest {
-  @ConfigSuite.Default
-  public static Config defaultConfig() {
-    return ElasticTestUtils.createConfig();
-  }
-
-  @ConfigSuite.Config
-  public static Config searchAfterPaginationType() {
-    Config config = defaultConfig();
-    config.setString("index", null, "paginationType", "SEARCH_AFTER");
-    return config;
-  }
-
-  private static ElasticContainer container;
-  private static CloseableHttpAsyncClient client;
-
+public class ElasticV7QueryProjectsTest extends ElasticAbstractQueryProjectsTest {
   @BeforeClass
   public static void startIndexService() {
-    container = ElasticContainer.createAndStart(ElasticVersion.V7_16);
-    client = ElasticTestUtils.createHttpAsyncClient(container);
-    client.start();
-  }
-
-  @AfterClass
-  public static void stopElasticsearchServer() {
-    if (container != null) {
-      container.stop();
-    }
-  }
-
-  @Override
-  protected void initAfterLifecycleStart() throws Exception {
-    super.initAfterLifecycleStart();
-    ElasticTestUtils.createAllIndexes(injector);
-  }
-
-  @Override
-  protected Injector createInjector() {
-    return ElasticTestUtils.createInjector(config, testName, container);
-  }
-
-  @Test
-  public void testErrorResponseFromProjectIndex() throws Exception {
-    ProjectApi project = gApi.projects().create("test");
-    project.index(false);
-
-    ElasticTestUtils.closeIndex(client, container, testName);
-    StorageException thrown = assertThrows(StorageException.class, () -> project.index(false));
-    assertThat(thrown).hasMessageThat().contains("Failed to replace project");
+    startIndexService(ElasticVersion.V7_16);
   }
 }
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryAccountsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryAccountsTest.java
new file mode 100644
index 0000000..b1ef2e5
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryAccountsTest.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import org.junit.BeforeClass;
+
+public class ElasticV8QueryAccountsTest extends ElasticAbstractQueryAccountsTest {
+  @BeforeClass
+  public static void startIndexService() {
+    startIndexService(ElasticVersion.V8_9);
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryChangesTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryChangesTest.java
new file mode 100644
index 0000000..a9a220d
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryChangesTest.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import org.junit.BeforeClass;
+
+public class ElasticV8QueryChangesTest extends ElasticAbstractQueryChangesTest {
+  @BeforeClass
+  public static void startIndexService() {
+    startIndexService(ElasticVersion.V8_9);
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryGroupsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryGroupsTest.java
new file mode 100644
index 0000000..37a204d
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryGroupsTest.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import org.junit.BeforeClass;
+
+public class ElasticV8QueryGroupsTest extends ElasticAbstractQueryGroupsTest {
+  @BeforeClass
+  public static void startIndexService() {
+    startIndexService(ElasticVersion.V8_9);
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryProjectsTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryProjectsTest.java
new file mode 100644
index 0000000..ace5767
--- /dev/null
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticV8QueryProjectsTest.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.elasticsearch;
+
+import org.junit.BeforeClass;
+
+public class ElasticV8QueryProjectsTest extends ElasticAbstractQueryProjectsTest {
+  @BeforeClass
+  public static void startIndexService() {
+    startIndexService(ElasticVersion.V8_9);
+  }
+}
diff --git a/src/test/java/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/src/test/java/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index ea7782b..b750466 100644
--- a/src/test/java/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/src/test/java/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -24,6 +24,10 @@
   public void supportedVersion() throws Exception {
     assertThat(ElasticVersion.forVersion("7.16.0")).isEqualTo(ElasticVersion.V7_16);
     assertThat(ElasticVersion.forVersion("7.16.1")).isEqualTo(ElasticVersion.V7_16);
+
+    assertThat(ElasticVersion.forVersion("8.9.0")).isEqualTo(ElasticVersion.V8_9);
+    assertThat(ElasticVersion.forVersion("8.9.1")).isEqualTo(ElasticVersion.V8_9);
+    assertThat(ElasticVersion.forVersion("8.9.2")).isEqualTo(ElasticVersion.V8_9);
   }
 
   @Test