From a99f7bb87d9db481db32102d07c1cf15ab57cc51 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Tue, 24 Jun 2025 22:03:34 -0400 Subject: [PATCH] Add test dependencies for FoundationDB --- pom.xml | 6 ++ service/pom.xml | 7 +++ .../FoundationDbDatabaseLifecycleManager.java | 19 ++++++ .../storage/FoundationDbExtension.java | 43 +++++++++++++ .../storage/FoundationDbTest.java | 34 ++++++++++ ...rFoundationDbDatabaseLifecycleManager.java | 63 +++++++++++++++++++ ...sFoundationDbDatabaseLifecycleManager.java | 45 +++++++++++++ 7 files changed, 217 insertions(+) create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbDatabaseLifecycleManager.java create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbExtension.java create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbTest.java create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/storage/ServiceContainerFoundationDbDatabaseLifecycleManager.java create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/storage/TestcontainersFoundationDbDatabaseLifecycleManager.java diff --git a/pom.xml b/pom.xml index 84b39cead..61c851e3c 100644 --- a/pom.xml +++ b/pom.xml @@ -332,6 +332,12 @@ pom import + + earth.adi + testcontainers-foundationdb + 1.1.0 + test + diff --git a/service/pom.xml b/service/pom.xml index 41a7e6718..40ce2b6ee 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -502,6 +502,12 @@ test + + earth.adi + testcontainers-foundationdb + test + + com.google.auth google-auth-library-oauth2-http @@ -733,6 +739,7 @@ -javaagent:${org.mockito:mockito-core:jar} --add-opens=java.base/java.net=ALL-UNNAMED + ${foundationdb.version} ${localstack.image} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbDatabaseLifecycleManager.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbDatabaseLifecycleManager.java new file mode 100644 index 000000000..96bfa113c --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbDatabaseLifecycleManager.java @@ -0,0 +1,19 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.apple.foundationdb.Database; +import com.apple.foundationdb.FDB; +import java.io.IOException; + +interface FoundationDbDatabaseLifecycleManager { + + void initializeDatabase(final FDB fdb) throws IOException; + + Database getDatabase(); + + void closeDatabase(); +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbExtension.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbExtension.java new file mode 100644 index 000000000..9c87e471d --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbExtension.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.apple.foundationdb.Database; +import com.apple.foundationdb.FDB; +import java.io.IOException; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +class FoundationDbExtension implements BeforeAllCallback, ExtensionContext.Store.CloseableResource { + + private static FoundationDbDatabaseLifecycleManager databaseLifecycleManager; + + @Override + public void beforeAll(final ExtensionContext context) throws IOException { + if (databaseLifecycleManager == null) { + final String serviceContainerName = System.getProperty("foundationDb.serviceContainerName"); + + databaseLifecycleManager = serviceContainerName != null + ? new ServiceContainerFoundationDbDatabaseLifecycleManager(serviceContainerName) + : new TestcontainersFoundationDbDatabaseLifecycleManager(); + + databaseLifecycleManager.initializeDatabase(FDB.selectAPIVersion(730)); + + context.getRoot().getStore(ExtensionContext.Namespace.GLOBAL).put(getClass().getName(), this); + } + } + + public Database getDatabase() { + return databaseLifecycleManager.getDatabase(); + } + + @Override + public void close() throws Throwable { + if (databaseLifecycleManager != null) { + databaseLifecycleManager.closeDatabase(); + } + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbTest.java new file mode 100644 index 000000000..f19d509bb --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.whispersystems.textsecuregcm.util.TestRandomUtil; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class FoundationDbTest { + + @RegisterExtension + static FoundationDbExtension FOUNDATION_DB_EXTENSION = new FoundationDbExtension(); + + @Test + void setGetValue() { + final byte[] key = "test".getBytes(StandardCharsets.UTF_8); + final byte[] value = TestRandomUtil.nextBytes(16); + + FOUNDATION_DB_EXTENSION.getDatabase().run(transaction -> { + transaction.set(key, value); + return null; + }); + + final byte[] retrievedValue = FOUNDATION_DB_EXTENSION.getDatabase().run(transaction -> transaction.get(key).join()); + + assertArrayEquals(value, retrievedValue); + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/ServiceContainerFoundationDbDatabaseLifecycleManager.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/ServiceContainerFoundationDbDatabaseLifecycleManager.java new file mode 100644 index 000000000..973771d17 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/ServiceContainerFoundationDbDatabaseLifecycleManager.java @@ -0,0 +1,63 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.apple.foundationdb.Database; +import com.apple.foundationdb.FDB; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages the lifecycle of a database connected to a FoundationDB instance running as an external service container. + */ +class ServiceContainerFoundationDbDatabaseLifecycleManager implements FoundationDbDatabaseLifecycleManager { + + private final String foundationDbServiceContainerName; + + private Database database; + + private static final Logger log = LoggerFactory.getLogger(ServiceContainerFoundationDbDatabaseLifecycleManager.class); + + ServiceContainerFoundationDbDatabaseLifecycleManager(final String foundationDbServiceContainerName) { + log.info("Using FoundationDB service container: {}", foundationDbServiceContainerName); + this.foundationDbServiceContainerName = foundationDbServiceContainerName; + } + + @Override + public void initializeDatabase(final FDB fdb) throws IOException { + final File clusterFile = File.createTempFile("fdb.cluster", ""); + clusterFile.deleteOnExit(); + + try (final FileWriter fileWriter = new FileWriter(clusterFile)) { + fileWriter.write(String.format("docker:docker@%s:4500", foundationDbServiceContainerName)); + } + + // If we don't initialize the database before trying to use it, things will just hang in a mysterious, message-free + // way. Note that the `new` keyword in `configure new single memory` means that we can't accidentally clobber an + // existing database (though initialization may fail if there's already a database present). + new ProcessBuilder("/usr/bin/fdbcli", + "-C", clusterFile.getAbsolutePath(), + "--exec", "configure new single memory") + .start() + .onExit() + .join(); + + database = fdb.open(clusterFile.getAbsolutePath()); + } + + @Override + public Database getDatabase() { + return database; + } + + @Override + public void closeDatabase() { + database.close(); + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/TestcontainersFoundationDbDatabaseLifecycleManager.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/TestcontainersFoundationDbDatabaseLifecycleManager.java new file mode 100644 index 000000000..cf458d8ad --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/TestcontainersFoundationDbDatabaseLifecycleManager.java @@ -0,0 +1,45 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.apple.foundationdb.Database; +import com.apple.foundationdb.FDB; +import earth.adi.testcontainers.containers.FoundationDBContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.utility.DockerImageName; + +class TestcontainersFoundationDbDatabaseLifecycleManager implements FoundationDbDatabaseLifecycleManager { + + private FoundationDBContainer foundationDBContainer; + private Database database; + + private static final String FOUNDATIONDB_IMAGE_NAME = "foundationdb/foundationdb:" + + System.getProperty("foundationdb.version", "7.3.62"); + + private static final Logger log = LoggerFactory.getLogger(TestcontainersFoundationDbDatabaseLifecycleManager.class); + + @Override + public void initializeDatabase(final FDB fdb) { + log.info("Using Testcontainers FoundationDB container: {}", FOUNDATIONDB_IMAGE_NAME); + + foundationDBContainer = new FoundationDBContainer(DockerImageName.parse(FOUNDATIONDB_IMAGE_NAME)); + foundationDBContainer.start(); + + database = fdb.open(foundationDBContainer.getClusterFilePath()); + } + + @Override + public Database getDatabase() { + return database; + } + + @Override + public void closeDatabase() { + database.close(); + foundationDBContainer.close(); + } +}