dynamicConfigurationManager = configuration.getAppConfig().build(
+ DynamicConfiguration.class, dynamicConfigurationExecutor, awsCredentialsProvider);
dynamicConfigurationManager.start();
MetricsUtil.configureRegistries(configuration, environment, dynamicConfigurationManager);
final ClientResources.Builder redisClientResourcesBuilder = ClientResources.builder();
- FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache",
- configuration.getCacheClusterConfiguration(), redisClientResourcesBuilder);
+ FaultTolerantRedisCluster cacheCluster = configuration.getCacheClusterConfiguration().build("main_cache",
+ redisClientResourcesBuilder);
ScheduledExecutorService recurringJobExecutor = environment.lifecycle()
.scheduledExecutorService(name(name, "recurringJob-%d")).threads(2).build();
@@ -124,11 +124,11 @@ record CommandDependencies(
ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
configuration.getSvr2Configuration());
- DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(
- configuration.getDynamoDbClientConfiguration(), WhisperServerService.AWSSDK_CREDENTIALS_PROVIDER);
+ DynamoDbAsyncClient dynamoDbAsyncClient = configuration.getDynamoDbClientConfiguration()
+ .buildAsyncClient(awsCredentialsProvider);
- DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client(
- configuration.getDynamoDbClientConfiguration(), WhisperServerService.AWSSDK_CREDENTIALS_PROVIDER);
+ DynamoDbClient dynamoDbClient = configuration.getDynamoDbClientConfiguration()
+ .buildSyncClient(awsCredentialsProvider);
RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
configuration.getDynamoDbTables().getRegistrationRecovery().getTableName(),
@@ -163,12 +163,12 @@ record CommandDependencies(
configuration.getDynamoDbTables().getMessages().getTableName(),
configuration.getDynamoDbTables().getMessages().getExpiration(),
messageDeletionExecutor);
- FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages",
- configuration.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClientResourcesBuilder);
- FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence",
- configuration.getClientPresenceClusterConfiguration(), redisClientResourcesBuilder);
- FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters",
- configuration.getRateLimitersCluster(), redisClientResourcesBuilder);
+ FaultTolerantRedisCluster messagesCluster = configuration.getMessageCacheConfiguration()
+ .getRedisClusterConfiguration().build("messages", redisClientResourcesBuilder);
+ FaultTolerantRedisCluster clientPresenceCluster = configuration.getClientPresenceClusterConfiguration()
+ .build("client_presence", redisClientResourcesBuilder);
+ FaultTolerantRedisCluster rateLimitersCluster = configuration.getRateLimitersCluster().build("rate_limiters",
+ redisClientResourcesBuilder);
SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client(
secureValueRecoveryCredentialsGenerator, secureValueRecoveryServiceExecutor,
secureValueRecoveryServiceRetryExecutor,
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/ScheduledApnPushNotificationSenderServiceCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/ScheduledApnPushNotificationSenderServiceCommand.java
index 5f61686ce..26dfa09c1 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/ScheduledApnPushNotificationSenderServiceCommand.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/ScheduledApnPushNotificationSenderServiceCommand.java
@@ -63,8 +63,8 @@ public class ScheduledApnPushNotificationSenderServiceCommand extends ServerComm
});
}
- final FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler",
- configuration.getPushSchedulerCluster(), deps.redisClusterClientResourcesBuilder());
+ final FaultTolerantRedisCluster pushSchedulerCluster = configuration.getPushSchedulerCluster()
+ .build("push_scheduler", deps.redisClusterClientResourcesBuilder());
final ExecutorService apnSenderExecutor = environment.lifecycle().executorService(name(getClass(), "apnSender-%d"))
.maxThreads(1).minThreads(1).build();
diff --git a/service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
new file mode 100644
index 000000000..bc79249e9
--- /dev/null
+++ b/service/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
@@ -0,0 +1,11 @@
+org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory
+org.whispersystems.textsecuregcm.configuration.DatadogConfiguration
+org.whispersystems.textsecuregcm.configuration.DynamicConfigurationManagerFactory
+org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory
+org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory
+org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClusterFactory
+org.whispersystems.textsecuregcm.configuration.PaymentsServiceClientsFactory
+org.whispersystems.textsecuregcm.configuration.PubSubPublisherFactory
+org.whispersystems.textsecuregcm.configuration.RegistrationServiceClientFactory
+org.whispersystems.textsecuregcm.configuration.S3ObjectMonitorFactory
+org.whispersystems.textsecuregcm.configuration.SingletonRedisClientFactory
diff --git a/service/src/main/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory b/service/src/main/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory
new file mode 100644
index 000000000..51bf26954
--- /dev/null
+++ b/service/src/main/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.StaticAwsCredentialsFactory
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/LocalWhisperServerService.java b/service/src/test/java/org/whispersystems/textsecuregcm/LocalWhisperServerService.java
new file mode 100644
index 000000000..ebf83d868
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/LocalWhisperServerService.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm;
+
+import io.dropwizard.util.Resources;
+
+/**
+ * This class may be run directly from a correctly configured IDE, or using the command line:
+ *
+ * ./mvnw clean integration-test -DskipTests=true -Ptest-server
+ *
+ * NOTE: many features are non-functional, especially those that depend on external services
+ */
+public class LocalWhisperServerService {
+
+ public static void main(String[] args) throws Exception {
+
+ System.setProperty("secrets.bundle.filename",
+ Resources.getResource("config/test-secrets-bundle.yml").getPath());
+ System.setProperty("sqlite.dir", "service/target/lib");
+ System.setProperty("aws.region", "local-test-region");
+
+ new WhisperServerService().run("server", Resources.getResource("config/test.yml").getPath());
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/WhisperServerServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/WhisperServerServiceTest.java
new file mode 100644
index 000000000..e816a3261
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/WhisperServerServiceTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.dropwizard.testing.junit5.DropwizardAppExtension;
+import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
+import io.dropwizard.util.Resources;
+import java.net.URI;
+import java.util.Map;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.core.Response;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.whispersystems.textsecuregcm.storage.DynamoDbExtension;
+import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema;
+import org.whispersystems.textsecuregcm.tests.util.TestWebsocketListener;
+import org.whispersystems.textsecuregcm.util.AttributeValues;
+import org.whispersystems.websocket.messages.WebSocketResponseMessage;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
+import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
+
+@ExtendWith(DropwizardExtensionsSupport.class)
+class WhisperServerServiceTest {
+
+ static {
+ System.setProperty("secrets.bundle.filename",
+ Resources.getResource("config/test-secrets-bundle.yml").getPath());
+ // needed for AppConfigDataClient initialization
+ System.setProperty("aws.region", "local-test-region");
+ }
+
+ private static final DropwizardAppExtension EXTENSION = new DropwizardAppExtension<>(
+ WhisperServerService.class, Resources.getResource("config/test.yml").getPath());
+
+ private WebSocketClient webSocketClient;
+
+ @AfterAll
+ static void teardown() {
+ System.clearProperty("secrets.bundle.filename");
+ System.clearProperty("aws.region");
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ webSocketClient = new WebSocketClient();
+ webSocketClient.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ webSocketClient.stop();
+ }
+
+ @Test
+ void start() throws Exception {
+ // make sure the service nominally starts and responds to health checks
+
+ Client client = EXTENSION.client();
+
+ final Response ping = client.target(
+ String.format("http://localhost:%d%s", EXTENSION.getAdminPort(), "/ping"))
+ .request("application/json")
+ .get();
+
+ assertEquals(200, ping.getStatus());
+
+ final Response healthCheck = client.target(
+ String.format("http://localhost:%d%s", EXTENSION.getLocalPort(), "/health-check"))
+ .request("application/json")
+ .get();
+
+ assertEquals(200, healthCheck.getStatus());
+ }
+
+ @Test
+ void websocket() throws Exception {
+ // test unauthenticated websocket
+
+ final TestWebsocketListener testWebsocketListener = new TestWebsocketListener();
+ webSocketClient.connect(testWebsocketListener,
+ URI.create(String.format("ws://localhost:%d/v1/websocket/", EXTENSION.getLocalPort())))
+ .join();
+
+ final WebSocketResponseMessage keepAlive = testWebsocketListener.doGet("/v1/keepalive").join();
+
+ assertEquals(200, keepAlive.getStatus());
+ }
+
+ @Test
+ void dynamoDb() {
+ // confirm that local dynamodb nominally works
+
+ final AwsCredentialsProvider awsCredentialsProvider = EXTENSION.getConfiguration().getAwsCredentialsConfiguration()
+ .build();
+
+ try (DynamoDbClient dynamoDbClient = EXTENSION.getConfiguration().getDynamoDbClientConfiguration()
+ .buildSyncClient(awsCredentialsProvider)) {
+
+ final DynamoDbExtension.TableSchema numbers = DynamoDbExtensionSchema.Tables.NUMBERS;
+ final AttributeValue numberAV = AttributeValues.s("+12125550001");
+
+ final GetItemResponse notFoundResponse = dynamoDbClient.getItem(GetItemRequest.builder()
+ .tableName(numbers.tableName())
+ .key(Map.of(numbers.hashKeyName(), numberAV))
+ .build());
+
+ assertFalse(notFoundResponse.hasItem());
+
+ dynamoDbClient.putItem(PutItemRequest.builder()
+ .tableName(numbers.tableName())
+ .item(Map.of(numbers.hashKeyName(), numberAV))
+ .build());
+
+ final GetItemResponse foundResponse = dynamoDbClient.getItem(GetItemRequest.builder()
+ .tableName(numbers.tableName())
+ .key(Map.of(numbers.hashKeyName(), numberAV))
+ .build());
+
+ assertTrue(foundResponse.hasItem());
+
+ dynamoDbClient.deleteItem(DeleteItemRequest.builder()
+ .tableName(numbers.tableName())
+ .key(Map.of(numbers.hashKeyName(), numberAV))
+ .build());
+ }
+
+ }
+
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/LocalDynamoDbFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/LocalDynamoDbFactory.java
new file mode 100644
index 000000000..3cfeae1dd
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/LocalDynamoDbFactory.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.whispersystems.textsecuregcm.storage.DynamoDbExtension;
+import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+
+@JsonTypeName("local")
+public class LocalDynamoDbFactory implements DynamoDbClientFactory {
+
+ private static final DynamoDbExtension EXTENSION = new DynamoDbExtension(System.getProperty("sqlite.dir"),
+ DynamoDbExtensionSchema.Tables.values());
+
+ static {
+ try {
+ EXTENSION.beforeEach(null);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> EXTENSION.afterEach(null)));
+ }
+
+ @Override
+ public DynamoDbClient buildSyncClient(final AwsCredentialsProvider awsCredentialsProvider) {
+ return EXTENSION.getDynamoDbClient();
+ }
+
+ @Override
+ public DynamoDbAsyncClient buildAsyncClient(final AwsCredentialsProvider awsCredentialsProvider) {
+ return EXTENSION.getDynamoDbAsyncClient();
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/LocalFaultTolerantRedisClusterFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/LocalFaultTolerantRedisClusterFactory.java
new file mode 100644
index 000000000..f9e181cad
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/LocalFaultTolerantRedisClusterFactory.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import io.lettuce.core.resource.ClientResources;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
+import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
+
+@JsonTypeName("local")
+public class LocalFaultTolerantRedisClusterFactory implements FaultTolerantRedisClusterFactory {
+
+ private static final RedisClusterExtension redisClusterExtension = RedisClusterExtension.builder().build();
+
+ private final AtomicBoolean shutdownHookConfigured = new AtomicBoolean();
+
+ private LocalFaultTolerantRedisClusterFactory() {
+ try {
+ redisClusterExtension.beforeAll(null);
+ redisClusterExtension.beforeEach(null);
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public FaultTolerantRedisCluster build(final String name, final ClientResources.Builder clientResourcesBuilder) {
+
+ if (shutdownHookConfigured.compareAndSet(false, true)) {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ redisClusterExtension.afterEach(null);
+ redisClusterExtension.afterAll(null);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+
+ final RedisClusterConfiguration config = new RedisClusterConfiguration();
+ config.setConfigurationUri(RedisClusterExtension.getRedisURIs().getFirst().toString());
+
+ return new FaultTolerantRedisCluster(name, config, clientResourcesBuilder);
+ }
+
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/LocalSingletonRedisClientFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/LocalSingletonRedisClientFactory.java
new file mode 100644
index 000000000..c17e2706a
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/LocalSingletonRedisClientFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import io.dropwizard.lifecycle.Managed;
+import io.lettuce.core.RedisClient;
+import io.lettuce.core.resource.ClientResources;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.whispersystems.textsecuregcm.redis.RedisSingletonExtension;
+
+@JsonTypeName("local")
+public class LocalSingletonRedisClientFactory implements SingletonRedisClientFactory, Managed {
+
+ private static final RedisSingletonExtension redisSingletonExtension = RedisSingletonExtension.builder().build();
+
+ private final AtomicBoolean shutdownHookConfigured = new AtomicBoolean();
+
+ private LocalSingletonRedisClientFactory() {
+ try {
+ redisSingletonExtension.beforeAll(null);
+ redisSingletonExtension.beforeEach(null);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public RedisClient build(final ClientResources clientResources) {
+
+ if (shutdownHookConfigured.compareAndSet(false, true)) {
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ try {
+ this.stop();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }));
+ }
+
+ return RedisClient.create(clientResources, redisSingletonExtension.getRedisUri());
+ }
+
+ @Override
+ public void stop() throws Exception {
+ redisSingletonExtension.afterEach(null);
+ redisSingletonExtension.afterAll(null);
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/NoWaitDogstatsdConfiguration.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/NoWaitDogstatsdConfiguration.java
new file mode 100644
index 000000000..fcdae4461
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/NoWaitDogstatsdConfiguration.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import java.time.Duration;
+
+@JsonTypeName("nowait")
+public class NoWaitDogstatsdConfiguration extends DogstatsdConfiguration {
+
+ @Override
+ public Duration getShutdownWaitDuration() {
+ return Duration.ZERO;
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StaticDynamicConfigurationManagerFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StaticDynamicConfigurationManagerFactory.java
new file mode 100644
index 000000000..d01f49bfb
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StaticDynamicConfigurationManagerFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import java.util.concurrent.ScheduledExecutorService;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotEmpty;
+import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+
+@JsonTypeName("static")
+public class StaticDynamicConfigurationManagerFactory implements DynamicConfigurationManagerFactory {
+
+ @JsonProperty
+ @NotEmpty
+ private String application;
+
+ @JsonProperty
+ @NotEmpty
+ private String environment;
+
+ @JsonProperty
+ @NotEmpty
+ private String configuration;
+
+ @JsonProperty
+ @NotBlank
+ private String staticConfig;
+
+ @Override
+ public DynamicConfigurationManager build(final Class klazz,
+ final ScheduledExecutorService scheduledExecutorService, final AwsCredentialsProvider awsCredentialsProvider) {
+
+ return new StaticDynamicConfigurationManager<>(staticConfig, application, environment, configuration,
+ awsCredentialsProvider, klazz, scheduledExecutorService);
+ }
+
+ private static class StaticDynamicConfigurationManager extends DynamicConfigurationManager {
+
+ private final T configuration;
+
+ public StaticDynamicConfigurationManager(final String config, final String application, final String environment,
+ final String configurationName, final AwsCredentialsProvider awsCredentialsProvider,
+ final Class configurationClass, final ScheduledExecutorService scheduledExecutorService) {
+
+ super(application, environment, configurationName, awsCredentialsProvider, configurationClass,
+ scheduledExecutorService);
+
+ try {
+ this.configuration = parseConfiguration(config, configurationClass).orElseThrow();
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public T getConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public void start() {
+ // do nothing
+ }
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StaticS3ObjectMonitorFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StaticS3ObjectMonitorFactory.java
new file mode 100644
index 000000000..de2a4e328
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StaticS3ObjectMonitorFactory.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.whispersystems.textsecuregcm.s3.S3ObjectMonitor;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+
+@JsonTypeName("static")
+public class StaticS3ObjectMonitorFactory implements S3ObjectMonitorFactory {
+
+ @JsonProperty
+ private byte[] object = new byte[0];
+
+ @Override
+ public S3ObjectMonitor build(final AwsCredentialsProvider awsCredentialsProvider,
+ final ScheduledExecutorService refreshExecutorService) {
+ return new StaticS3ObjectMonitor(object, awsCredentialsProvider);
+ }
+
+ private static class StaticS3ObjectMonitor extends S3ObjectMonitor {
+
+ private final byte[] object;
+
+ public StaticS3ObjectMonitor(final byte[] object, final AwsCredentialsProvider awsCredentialsProvider) {
+ super(awsCredentialsProvider, "local-test-region", "test-bucket", null, 0L, null, null);
+
+ this.object = object;
+ }
+
+ @Override
+ public synchronized void start(final Consumer changeListener) {
+ changeListener.accept(new ByteArrayInputStream(object));
+ }
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubHCaptchaClientFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubHCaptchaClientFactory.java
new file mode 100644
index 000000000..d68f42b7d
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubHCaptchaClientFactory.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import java.io.IOException;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import org.whispersystems.textsecuregcm.captcha.Action;
+import org.whispersystems.textsecuregcm.captcha.AssessmentResult;
+import org.whispersystems.textsecuregcm.captcha.HCaptchaClient;
+import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
+import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
+
+@JsonTypeName("stub")
+public class StubHCaptchaClientFactory implements HCaptchaClientFactory {
+
+ @Override
+ public HCaptchaClient build(final ScheduledExecutorService retryExecutor,
+ final DynamicConfigurationManager dynamicConfigurationManager) {
+
+ return new StubHCaptchaClient(retryExecutor, new CircuitBreakerConfiguration(), dynamicConfigurationManager);
+ }
+
+ /**
+ * Accepts any token of the format "test.test.*.*"
+ */
+ private static class StubHCaptchaClient extends HCaptchaClient {
+
+ public StubHCaptchaClient(final ScheduledExecutorService retryExecutor,
+ final CircuitBreakerConfiguration circuitBreakerConfiguration,
+ final DynamicConfigurationManager dynamicConfigurationManager) {
+ super(null, retryExecutor, circuitBreakerConfiguration, null, dynamicConfigurationManager);
+ }
+
+ @Override
+ public String scheme() {
+ return "test";
+ }
+
+ @Override
+ public Set validSiteKeys(final Action action) {
+ return Set.of("test");
+ }
+
+ @Override
+ public AssessmentResult verify(final String siteKey, final Action action, final String token, final String ip)
+ throws IOException {
+ return AssessmentResult.alwaysValid();
+ }
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPaymentsServiceClientsFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPaymentsServiceClientsFactory.java
new file mode 100644
index 000000000..28d1423ba
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPaymentsServiceClientsFactory.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import java.math.BigDecimal;
+import java.net.http.HttpClient;
+import java.util.Collections;
+import java.util.Map;
+import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient;
+import org.whispersystems.textsecuregcm.currency.FixerClient;
+
+@JsonTypeName("stub")
+public class StubPaymentsServiceClientsFactory implements PaymentsServiceClientsFactory {
+
+ @Override
+ public FixerClient buildFixerClient(final HttpClient httpClient) {
+ return new StubFixerClient();
+ }
+
+ @Override
+ public CoinMarketCapClient buildCoinMarketCapClient(final HttpClient httpClient) {
+ return new StubCoinMarketCapClient();
+ }
+
+ /**
+ * Always returns an empty map of conversions
+ */
+ private static class StubFixerClient extends FixerClient {
+
+ public StubFixerClient() {
+ super(null, null);
+ }
+
+ @Override
+ public Map getConversionsForBase(final String base) throws FixerException {
+ return Collections.emptyMap();
+ }
+ }
+
+ /**
+ * Always returns {@code 0} for spot price checks
+ */
+ private static class StubCoinMarketCapClient extends CoinMarketCapClient {
+
+ public StubCoinMarketCapClient() {
+ super(null, null, null);
+ }
+
+ @Override
+ public BigDecimal getSpotPrice(final String currency, final String base) {
+ return BigDecimal.ZERO;
+ }
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPubSubPublisherFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPubSubPublisherFactory.java
new file mode 100644
index 000000000..27cabfbda
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubPubSubPublisherFactory.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.google.api.core.ApiFutures;
+import com.google.cloud.pubsub.v1.PublisherInterface;
+import java.util.UUID;
+
+@JsonTypeName("stub")
+public class StubPubSubPublisherFactory implements PubSubPublisherFactory {
+
+ @Override
+ public PublisherInterface build() {
+ return message -> ApiFutures.immediateFuture(UUID.randomUUID().toString());
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubRegistrationServiceClientFactory.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubRegistrationServiceClientFactory.java
new file mode 100644
index 000000000..bdb73cd52
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/StubRegistrationServiceClientFactory.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber;
+import io.dropwizard.core.setup.Environment;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import javax.validation.constraints.NotNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
+import org.whispersystems.textsecuregcm.registration.ClientType;
+import org.whispersystems.textsecuregcm.registration.MessageTransport;
+import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
+
+@JsonTypeName("stub")
+public class StubRegistrationServiceClientFactory implements RegistrationServiceClientFactory {
+
+ @JsonProperty
+ @NotNull
+ private String registrationCaCertificate;
+
+ @Override
+ public RegistrationServiceClient build(final Environment environment, final Executor callbackExecutor,
+ final ScheduledExecutorService identityRefreshExecutor) {
+
+ try {
+ return new StubRegistrationServiceClient(registrationCaCertificate);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class StubRegistrationServiceClient extends RegistrationServiceClient {
+
+ private final static Map SESSIONS = new ConcurrentHashMap<>();
+
+ public StubRegistrationServiceClient(final String registrationCaCertificate) throws IOException {
+ super("example.com", 8080, null, registrationCaCertificate, null);
+ }
+
+ @Override
+ public CompletableFuture createRegistrationSession(
+ final Phonenumber.PhoneNumber phoneNumber, final boolean accountExistsWithPhoneNumber, final Duration timeout) {
+
+ final String e164 = PhoneNumberUtil.getInstance()
+ .format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
+
+ final byte[] id = new byte[32];
+ new SecureRandom().nextBytes(id);
+ final RegistrationServiceSession session = new RegistrationServiceSession(id, e164, false, 0L, 0L, null,
+ Instant.now().plus(Duration.ofMinutes(10)).toEpochMilli());
+ SESSIONS.put(Base64.getEncoder().encodeToString(id), session);
+
+ return CompletableFuture.completedFuture(session);
+ }
+
+ @Override
+ public CompletableFuture sendVerificationCode(final byte[] sessionId,
+ final MessageTransport messageTransport, final ClientType clientType, final @Nullable String acceptLanguage,
+ final @Nullable String senderOverride, final Duration timeout) {
+ return CompletableFuture.completedFuture(SESSIONS.get(Base64.getEncoder().encodeToString(sessionId)));
+ }
+
+ @Override
+ public CompletableFuture checkVerificationCode(final byte[] sessionId,
+ final String verificationCode, final Duration timeout) {
+ final RegistrationServiceSession session = SESSIONS.get(Base64.getEncoder().encodeToString(sessionId));
+
+ final RegistrationServiceSession updatedSession = new RegistrationServiceSession(sessionId, session.number(),
+ true, 0L, 0L, 0L,
+ Instant.now().plus(Duration.ofMinutes(10)).toEpochMilli());
+
+ SESSIONS.put(Base64.getEncoder().encodeToString(sessionId), updatedSession);
+ return CompletableFuture.completedFuture(updatedSession);
+ }
+
+ @Override
+ public CompletableFuture> getSession(final byte[] sessionId,
+ final Duration timeout) {
+ return CompletableFuture.completedFuture(
+ Optional.ofNullable(SESSIONS.get(Base64.getEncoder().encodeToString(sessionId))));
+ }
+ }
+
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/push/ProvisioningManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/push/ProvisioningManagerTest.java
index 30067d7da..72e1e8cc0 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/push/ProvisioningManagerTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/ProvisioningManagerTest.java
@@ -8,7 +8,6 @@ import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import com.google.protobuf.ByteString;
-import java.time.Duration;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -32,7 +31,7 @@ class ProvisioningManagerTest {
@BeforeEach
void setUp() throws Exception {
- provisioningManager = new ProvisioningManager(REDIS_EXTENSION.getRedisClient(), Duration.ofSeconds(1), new CircuitBreakerConfiguration());
+ provisioningManager = new ProvisioningManager(REDIS_EXTENSION.getRedisClient(), new CircuitBreakerConfiguration());
provisioningManager.start();
}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisSingletonExtension.java b/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisSingletonExtension.java
index cf1b28eb2..eb8d4ed3d 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisSingletonExtension.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/redis/RedisSingletonExtension.java
@@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.redis;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import io.lettuce.core.RedisClient;
+import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import java.io.IOException;
import java.net.ServerSocket;
@@ -22,6 +23,7 @@ public class RedisSingletonExtension implements BeforeAllCallback, BeforeEachCal
private static RedisServer redisServer;
private RedisClient redisClient;
+ private RedisURI redisUri;
public static class RedisSingletonExtensionBuilder {
@@ -53,7 +55,8 @@ public class RedisSingletonExtension implements BeforeAllCallback, BeforeEachCal
@Override
public void beforeEach(final ExtensionContext context) {
- redisClient = RedisClient.create(String.format("redis://127.0.0.1:%d", redisServer.ports().get(0)));
+ redisUri = RedisURI.create("redis://127.0.0.1:%d".formatted(redisServer.ports().get(0)));
+ redisClient = RedisClient.create(redisUri);
try (final StatefulRedisConnection connection = redisClient.connect()) {
connection.sync().flushall();
@@ -76,6 +79,10 @@ public class RedisSingletonExtension implements BeforeAllCallback, BeforeEachCal
return redisClient;
}
+ public RedisURI getRedisUri() {
+ return redisUri;
+ }
+
private static int getAvailablePort() throws IOException {
try (ServerSocket socket = new ServerSocket(0)) {
socket.setReuseAddress(false);
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/s3/S3ObjectMonitorTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/s3/S3ObjectMonitorTest.java
index cea5b33c2..c20427acb 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/s3/S3ObjectMonitorTest.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/s3/S3ObjectMonitorTest.java
@@ -46,8 +46,7 @@ class S3ObjectMonitorTest {
objectKey,
16 * 1024 * 1024,
mock(ScheduledExecutorService.class),
- Duration.ofMinutes(1),
- listener);
+ Duration.ofMinutes(1));
final String uuid = UUID.randomUUID().toString();
when(s3Client.headObject(HeadObjectRequest.builder().bucket(bucket).key(objectKey).build())).thenReturn(
@@ -55,8 +54,8 @@ class S3ObjectMonitorTest {
final ResponseInputStream ris = responseInputStreamFromString("abc", uuid);
when(s3Client.getObject(GetObjectRequest.builder().bucket(bucket).key(objectKey).build())).thenReturn(ris);
- objectMonitor.refresh();
- objectMonitor.refresh();
+ objectMonitor.refresh(listener);
+ objectMonitor.refresh(listener);
verify(listener).accept(ris);
}
@@ -77,8 +76,7 @@ class S3ObjectMonitorTest {
objectKey,
16 * 1024 * 1024,
mock(ScheduledExecutorService.class),
- Duration.ofMinutes(1),
- listener);
+ Duration.ofMinutes(1));
final String uuid = UUID.randomUUID().toString();
when(s3Client.headObject(HeadObjectRequest.builder().key(objectKey).bucket(bucket).build()))
@@ -87,7 +85,7 @@ class S3ObjectMonitorTest {
when(s3Client.getObject(GetObjectRequest.builder().key(objectKey).bucket(bucket).build())).thenReturn(responseInputStream);
objectMonitor.getObject();
- objectMonitor.refresh();
+ objectMonitor.refresh(listener);
verify(listener, never()).accept(responseInputStream);
}
@@ -115,8 +113,7 @@ class S3ObjectMonitorTest {
objectKey,
maxObjectSize,
mock(ScheduledExecutorService.class),
- Duration.ofMinutes(1),
- listener);
+ Duration.ofMinutes(1));
final String uuid = UUID.randomUUID().toString();
when(s3Client.headObject(HeadObjectRequest.builder().bucket(bucket).key(objectKey).build())).thenReturn(
@@ -124,7 +121,7 @@ class S3ObjectMonitorTest {
final ResponseInputStream ris = responseInputStreamFromString("a".repeat((int) maxObjectSize+1), uuid);
when(s3Client.getObject(GetObjectRequest.builder().bucket(bucket).key(objectKey).build())).thenReturn(ris);
- objectMonitor.refresh();
+ objectMonitor.refresh(listener);
verify(listener, never()).accept(any());
}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtension.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtension.java
index bb35b5ecf..4f2c71706 100644
--- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtension.java
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/DynamoDbExtension.java
@@ -11,6 +11,7 @@ import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
import java.net.ServerSocket;
import java.net.URI;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
@@ -27,9 +28,12 @@ import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
import software.amazon.awssdk.services.dynamodb.model.KeyType;
import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex;
import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput;
+import javax.annotation.Nullable;
public class DynamoDbExtension implements BeforeEachCallback, AfterEachCallback {
+ private static final String DEFAULT_LIBRARY_PATH = "target/lib";
+
public interface TableSchema {
String tableName();
String hashKeyName();
@@ -58,22 +62,28 @@ public class DynamoDbExtension implements BeforeEachCallback, AfterEachCallback
private DynamoDBProxyServer server;
private int port;
+ private final String libraryPath;
private final List schemas;
private DynamoDbClient dynamoDB2;
private DynamoDbAsyncClient dynamoAsyncDB2;
public DynamoDbExtension(TableSchema... schemas) {
+ this(DEFAULT_LIBRARY_PATH, schemas);
+ }
+
+ public DynamoDbExtension(@Nullable final String libraryPath, TableSchema... schemas) {
+ this.libraryPath = Optional.ofNullable(libraryPath).orElse(DEFAULT_LIBRARY_PATH);
this.schemas = List.of(schemas);
}
- private static void loadLibrary() {
+ private void loadLibrary() {
// to avoid noise in the logs from “library already loaded” warnings, we make sure we only set it once
if (libraryLoaded.get()) {
return;
}
if (libraryLoaded.compareAndSet(false, true)) {
// if you see a library failed to load error, you need to run mvn test-compile at least once first
- SQLite.setLibraryPath("target/lib");
+ SQLite.setLibraryPath(this.libraryPath);
}
}
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.DatadogConfiguration b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.DatadogConfiguration
new file mode 100644
index 000000000..312e8e76b
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.DatadogConfiguration
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.NoWaitDogstatsdConfiguration
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.DynamicConfigurationManagerFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.DynamicConfigurationManagerFactory
new file mode 100644
index 000000000..9bc8ddf4d
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.DynamicConfigurationManagerFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.StaticDynamicConfigurationManagerFactory
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory
new file mode 100644
index 000000000..a412db4f1
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.LocalDynamoDbFactory
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClusterFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClusterFactory
new file mode 100644
index 000000000..8b83b743f
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClusterFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.LocalFaultTolerantRedisClusterFactory
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory
new file mode 100644
index 000000000..17d4ad3d3
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.HCaptchaClientFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.StubHCaptchaClientFactory
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.PaymentsServiceClientsFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.PaymentsServiceClientsFactory
new file mode 100644
index 000000000..77d25c67d
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.PaymentsServiceClientsFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.StubPaymentsServiceClientsFactory
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.PubSubPublisherFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.PubSubPublisherFactory
new file mode 100644
index 000000000..f46ac1088
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.PubSubPublisherFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.StubPubSubPublisherFactory
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.RegistrationServiceClientFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.RegistrationServiceClientFactory
new file mode 100644
index 000000000..3813423b7
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.RegistrationServiceClientFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.StubRegistrationServiceClientFactory
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.S3ObjectMonitorFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.S3ObjectMonitorFactory
new file mode 100644
index 000000000..05844241a
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.S3ObjectMonitorFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.StaticS3ObjectMonitorFactory
diff --git a/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.SingletonRedisClientFactory b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.SingletonRedisClientFactory
new file mode 100644
index 000000000..f851b9371
--- /dev/null
+++ b/service/src/test/resources/META-INF/services/org.whispersystems.textsecuregcm.configuration.SingletonRedisClientFactory
@@ -0,0 +1 @@
+org.whispersystems.textsecuregcm.configuration.LocalSingletonRedisClientFactory
diff --git a/service/src/test/resources/config/test-secrets-bundle.yml b/service/src/test/resources/config/test-secrets-bundle.yml
new file mode 100644
index 000000000..9299d4491
--- /dev/null
+++ b/service/src/test/resources/config/test-secrets-bundle.yml
@@ -0,0 +1,132 @@
+aws.accessKeyId: accessKey
+aws.secretAccessKey: secretAccess
+
+stripe.apiKey: unset
+stripe.idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
+
+braintree.privateKey: unset
+
+directoryV2.client.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users
+directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth identity tokens for Signal users
+
+svr2.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users
+svr2.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users
+
+svr3.userAuthenticationTokenSharedSecret: cbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR3 to generate auth tokens for Signal users
+svr3.userIdTokenSharedSecret: dbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR3 to generate auth identity tokens for Signal users
+
+tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
+
+awsAttachments.accessKey: test
+awsAttachments.accessSecret: test
+
+# The below private key was key generated exclusively for testing purposes. Do not use it in any other context.
+gcpAttachments.rsaSigningKey: |
+ -----BEGIN PRIVATE KEY-----
+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCrfHLw9zr/8mTX
+ c0YMN3P9pNLtn+JCsNx/6sz/7FYoJjH8CKG4zNgcJLATLGxQikTjD6yNDlgkpByD
+ qOmgXgZvIBBJadbbl+plJbU4kKwTRwdrYiq/ICMkVZBk5jfqYqSxzdw80ytj5Tha
+ 3M/3uqto7qELK91z/5cCC6pVsQXIrTqq4D41XyORKF2u4eeKOz3jiuXkdxRj4Vsb
+ MDwcS1WEi1ApoG50tDDn7e9mk3MAeE5L54ROHkd7FM471LRSU9ytpOzcH56tExLP
+ 21nN5vXZoyJnNvbgd1KZeZajjH+XHJS/wiqNAPEX2yvrFID4ECQMIonXtYyNDkmY
+ YxggNaCnAgMBAAECggEAFLDJStr+8A7BArXSh9AmWz4zLPSTiim+EQ5gJFN8Tw/S
+ DBob2SjuEkc4RLf2waj33XrwqNGdlPOFdTqWJavylB8xl99V9dzYgn0QO9OeJMf3
+ Kd+y+f3Yqkj188FLPH52Z0ryqGwaL3gNWqPge9VhWncgUIa/C4CVKcFakJ2b7bW2
+ NIk2bSMCNW8rptQZ+tWV9k86OAxjIocLbkpPgigRk6T3MAunMGVf6iviNSnOyOlZ
+ qmAPkRVs2uyK3Hnl0lEavaBW3KRs0ChU0rkfXHvGmi7V6aZ4rnG6OdRQiOgk3NYf
+ qQYqhnRMmN4st2WN6CDDdpk5o2pHR625Wqx11t/50QKBgQDmf+fYWKdQa8r+TO4w
+ 32JAiEdmFuA8fSEOaWyBik/NliJIPEApGMWLuZSmSzW80l4vt5zQ3LVgvRrxZv2y
+ 7odLxUP9jpFGVg3NpCB27nES+psmo7X4kXIfzPWGvkOs2HLpp8elVEPeOn7gkng9
+ XXXmB9vja8g/Jo9ym9FkigB0LQKBgQC+dTFTPvvVYFQ1KmeL94EOEL21ZXkgwjnx
+ 1BcnqK4p0M1NQ2xW1wwCljxlEQx5P6UY9HRWS6DecVpj6P7nRF2HWB+xsaO1aPZj
+ nMOETrUXGq8ksQml+0kI5f0A2w22wzpj3+kjiXSFBjxoWLAfKPHMKeUg/oYRfIVp
+ LeShMptIowKBgQC4H44U3ORyMlkKAGv4sEhs4i+elkFzMEU6nO4nIFQVFou2BiL+
+ cSJENe9PUx7PAYBpP5PNp7BfYU/na+zWhQGgfiiMn9jeRZlrHmMsfdXnYjaTjAyt
+ TYnLa07p3oxywsgwa2zoXUKFf1agj3/rDQBDyx1UMmHYSDYoR93hIPex1QKBgQCF
+ 4y6sna89ff1Ubp3iKDjiIWSre00eeUtwtC8e4xakMLPSZ95mYcCApQqJ5eVF6zbt
+ hxOtgnbxSPBJIgbnnwi813dYXE+AfOwQdKiBfy8QseKDwazNsQvTpJIqItPOMgn/
+ Ie3r3Ho79XlLxWTyUr9ATgdUHXk0G7xRh0CdDU1aTwKBgC5kDNr/R2XIWZL0TMzz
+ EVL2BkL11YumIpEBm+Hkx6fm3uCgR/ywMqplGdZcD+D5r0fUsckbOd1z6fFGAJqe
+ QJ3/4qaA+dcWPwB5GiKa1WIs48GJMyPrFciindEwr3BaDhhB9cEdxpVY2e/KEeZL
+ TQkqmVUmgKKvCFTPWwCgeIOD
+ -----END PRIVATE KEY-----
+
+apn.teamId: team-id
+apn.keyId: key-id
+# The below private key was key generated exclusively for testing purposes. Do not use it in any other context.
+apn.signingKey: |
+ -----BEGIN PRIVATE KEY-----
+ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxIXnNiHH35DDbKHY
+ 8kxoAYbukvMPVWN+kiIhZsFvqaahRANCAAQTWXjgagaLnTxcMJTUpO3rkhi8xjav
+ 7NSEd5L+df4M7V9YxxDoYY+UHd8B/KmrWR29SVIRLncSULgfSnHnHvoH
+ -----END PRIVATE KEY-----
+
+# The below private key was key generated exclusively for testing purposes. Do not use it in any other context.
+fcm.credentials: |
+ { "type": "service_account", "client_id": "client_id", "client_email": "fake@example.com",
+ "private_key_id": "id",
+ "private_key": "-----BEGIN PRIVATE KEY-----
+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCrfHLw9zr/8mTX
+ c0YMN3P9pNLtn+JCsNx/6sz/7FYoJjH8CKG4zNgcJLATLGxQikTjD6yNDlgkpByD
+ qOmgXgZvIBBJadbbl+plJbU4kKwTRwdrYiq/ICMkVZBk5jfqYqSxzdw80ytj5Tha
+ 3M/3uqto7qELK91z/5cCC6pVsQXIrTqq4D41XyORKF2u4eeKOz3jiuXkdxRj4Vsb
+ MDwcS1WEi1ApoG50tDDn7e9mk3MAeE5L54ROHkd7FM471LRSU9ytpOzcH56tExLP
+ 21nN5vXZoyJnNvbgd1KZeZajjH+XHJS/wiqNAPEX2yvrFID4ECQMIonXtYyNDkmY
+ YxggNaCnAgMBAAECggEAFLDJStr+8A7BArXSh9AmWz4zLPSTiim+EQ5gJFN8Tw/S
+ DBob2SjuEkc4RLf2waj33XrwqNGdlPOFdTqWJavylB8xl99V9dzYgn0QO9OeJMf3
+ Kd+y+f3Yqkj188FLPH52Z0ryqGwaL3gNWqPge9VhWncgUIa/C4CVKcFakJ2b7bW2
+ NIk2bSMCNW8rptQZ+tWV9k86OAxjIocLbkpPgigRk6T3MAunMGVf6iviNSnOyOlZ
+ qmAPkRVs2uyK3Hnl0lEavaBW3KRs0ChU0rkfXHvGmi7V6aZ4rnG6OdRQiOgk3NYf
+ qQYqhnRMmN4st2WN6CDDdpk5o2pHR625Wqx11t/50QKBgQDmf+fYWKdQa8r+TO4w
+ 32JAiEdmFuA8fSEOaWyBik/NliJIPEApGMWLuZSmSzW80l4vt5zQ3LVgvRrxZv2y
+ 7odLxUP9jpFGVg3NpCB27nES+psmo7X4kXIfzPWGvkOs2HLpp8elVEPeOn7gkng9
+ XXXmB9vja8g/Jo9ym9FkigB0LQKBgQC+dTFTPvvVYFQ1KmeL94EOEL21ZXkgwjnx
+ 1BcnqK4p0M1NQ2xW1wwCljxlEQx5P6UY9HRWS6DecVpj6P7nRF2HWB+xsaO1aPZj
+ nMOETrUXGq8ksQml+0kI5f0A2w22wzpj3+kjiXSFBjxoWLAfKPHMKeUg/oYRfIVp
+ LeShMptIowKBgQC4H44U3ORyMlkKAGv4sEhs4i+elkFzMEU6nO4nIFQVFou2BiL+
+ cSJENe9PUx7PAYBpP5PNp7BfYU/na+zWhQGgfiiMn9jeRZlrHmMsfdXnYjaTjAyt
+ TYnLa07p3oxywsgwa2zoXUKFf1agj3/rDQBDyx1UMmHYSDYoR93hIPex1QKBgQCF
+ 4y6sna89ff1Ubp3iKDjiIWSre00eeUtwtC8e4xakMLPSZ95mYcCApQqJ5eVF6zbt
+ hxOtgnbxSPBJIgbnnwi813dYXE+AfOwQdKiBfy8QseKDwazNsQvTpJIqItPOMgn/
+ Ie3r3Ho79XlLxWTyUr9ATgdUHXk0G7xRh0CdDU1aTwKBgC5kDNr/R2XIWZL0TMzz
+ EVL2BkL11YumIpEBm+Hkx6fm3uCgR/ywMqplGdZcD+D5r0fUsckbOd1z6fFGAJqe
+ QJ3/4qaA+dcWPwB5GiKa1WIs48GJMyPrFciindEwr3BaDhhB9cEdxpVY2e/KEeZL
+ TQkqmVUmgKKvCFTPWwCgeIOD
+ -----END PRIVATE KEY-----" }
+
+cdn.accessKey: test # AWS Access Key ID
+cdn.accessSecret: test # AWS Access Secret
+
+cdn3StorageManager.clientSecret: test
+
+# The below private key was key generated exclusively for testing purposes. Do not use it in any other context.
+# ca:
+# Public key : BVDuaR1ZT/5M26nSvFN1XjN4qFqVfwynt03l/GyK2GtP
+# Private key: 0Ie2/CIxfidwhS+uKSckb2YgtRR7UBLkecHNG2ARiW8=
+unidentifiedDelivery.certificate: CikIp/SIvwESIQWXK5ikKQdvB7XpwPowV4djRj6Ni0I9MO12AVNpzQ9DDBJApovqX/gmIc+ukn4YeYtCsk9Q0EoDoXAZPD0D5vflcnncUs2fFLEQLlnSZ/ckXVChZByyuiNiegtl468+A+u5gw==
+unidentifiedDelivery.privateKey: UKRdvrnxcy1hcILN7RHqUG40/mUd/ZnKkqhQToN4K1U=
+
+storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+
+# The below secrets were generated exclusively for testing purposes. Do not use them in any other context.
+zkConfig-libsignal-0.42.serverSecret: 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
+genericZkConfig.serverSecret: AIZmPk8ms6TWBTGFcFE1iEuu4kSpTRL1EAPA2ZVWm4EIIF/N811ZhILbCx8QSLBf90mNXhUtsfNF5PY5UdnJMgBGu3AtrVs5erRXf5hi6RxvCkl1QnYs/tcuUGNbkejyR9bPR2uJaK6CxGJS0RRUDWf8f2hQloe/+kWKilM1I/MHSV2+PcyCDJIigPi9RhbD2STXc6cHEpYXReg+1OYSEQk3K2M0qnUoVOAjbPuFXANEPU+106f37w/iF6MhyfWyDCb+oit29DFtoDS31cxheB3x1KVga2ErfnIyHpQrSWYHUdGPZLXc0xRmaa0VwDyyXzK0o3w4oS/F9+xqWYUWkwgsAm9e7dP4l0qVolnPQ67uNj7BFG4JQ0vXxD/JJQ+5B4bHyK+v5ndJpRMXDC9rJw8ehopvDCTXSoICqN7nvY8Fyqhf5zkM880Su2XiBa2paDTVuZgwq07zBeDrrPc2zQ8A4neV6++t95veOfpp94FymnHJ8ILaznKqzJluGDdtCA==
+callingZkConfig.serverSecret: AIZmPk8ms6TWBTGFcFE1iEuu4kSpTRL1EAPA2ZVWm4EIIF/N811ZhILbCx8QSLBf90mNXhUtsfNF5PY5UdnJMgBGu3AtrVs5erRXf5hi6RxvCkl1QnYs/tcuUGNbkejyR9bPR2uJaK6CxGJS0RRUDWf8f2hQloe/+kWKilM1I/MHSV2+PcyCDJIigPi9RhbD2STXc6cHEpYXReg+1OYSEQk3K2M0qnUoVOAjbPuFXANEPU+106f37w/iF6MhyfWyDCb+oit29DFtoDS31cxheB3x1KVga2ErfnIyHpQrSWYHUdGPZLXc0xRmaa0VwDyyXzK0o3w4oS/F9+xqWYUWkwgsAm9e7dP4l0qVolnPQ67uNj7BFG4JQ0vXxD/JJQ+5B4bHyK+v5ndJpRMXDC9rJw8ehopvDCTXSoICqN7nvY8Fyqhf5zkM880Su2XiBa2paDTVuZgwq07zBeDrrPc2zQ8A4neV6++t95veOfpp94FymnHJ8ILaznKqzJluGDdtCA==
+backupsZkConfig.serverSecret: AIZmPk8ms6TWBTGFcFE1iEuu4kSpTRL1EAPA2ZVWm4EIIF/N811ZhILbCx8QSLBf90mNXhUtsfNF5PY5UdnJMgBGu3AtrVs5erRXf5hi6RxvCkl1QnYs/tcuUGNbkejyR9bPR2uJaK6CxGJS0RRUDWf8f2hQloe/+kWKilM1I/MHSV2+PcyCDJIigPi9RhbD2STXc6cHEpYXReg+1OYSEQk3K2M0qnUoVOAjbPuFXANEPU+106f37w/iF6MhyfWyDCb+oit29DFtoDS31cxheB3x1KVga2ErfnIyHpQrSWYHUdGPZLXc0xRmaa0VwDyyXzK0o3w4oS/F9+xqWYUWkwgsAm9e7dP4l0qVolnPQ67uNj7BFG4JQ0vXxD/JJQ+5B4bHyK+v5ndJpRMXDC9rJw8ehopvDCTXSoICqN7nvY8Fyqhf5zkM880Su2XiBa2paDTVuZgwq07zBeDrrPc2zQ8A4neV6++t95veOfpp94FymnHJ8ILaznKqzJluGDdtCA==
+paymentsService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
+paymentsService.fixerApiKey: unset
+paymentsService.coinMarketCapApiKey: unset
+
+artService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret not shared with any external service, but used in ArtController
+artService.userAuthenticationTokenUserIdSecret: AAAAAAAAAAA= # base64-encoded secret to obscure user phone numbers from Sticker Creator
+
+currentReportingKey.secret: AAAAAAAAAAA=
+currentReportingKey.salt: AAAAAAAAAAA=
+
+turn.secret: AAAAAAAAAAA=
+
+linkDevice.secret: AAAAAAAAAAA=
+
+tlsKeyStore.password: unset
+
+noiseTunnel.recognizedProxySecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
diff --git a/service/src/test/resources/config/test.yml b/service/src/test/resources/config/test.yml
new file mode 100644
index 000000000..5a945b6f8
--- /dev/null
+++ b/service/src/test/resources/config/test.yml
@@ -0,0 +1,472 @@
+logging:
+ level: INFO
+ appenders:
+ - type: console
+ threshold: ALL
+ timeZone: UTC
+ target: stdout
+
+health:
+ delayedShutdownHandlerEnabled: false
+
+awsCredentialsProvider:
+ type: static
+ accessKeyId: secret://aws.accessKeyId
+ secretAccessKey: secret://aws.secretAccessKey
+
+metrics:
+ reporters:
+ - type: signal-datadog
+ frequency: 10 seconds
+ tags:
+ - "env:test"
+ - "service:chat"
+ udpTransport:
+ statsdHost: localhost
+ port: 8125
+ excludesAttributes:
+ - m1_rate
+ - m5_rate
+ - m15_rate
+ - mean_rate
+ - stddev
+ useRegexFilters: true
+ excludes:
+ - ^.+\.total$
+ - ^.+\.request\.filtering$
+ - ^.+\.response\.filtering$
+ - ^executor\..+$
+ - ^lettuce\..+$
+ reportOnStop: true
+
+tlsKeyStore:
+ password: secret://tlsKeyStore.password
+
+stripe:
+ apiKey: secret://stripe.apiKey
+ idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
+ boostDescription: >
+ Example
+ supportedCurrenciesByPaymentMethod:
+ CARD:
+ - usd
+ - eur
+ SEPA_DEBIT:
+ - eur
+
+braintree:
+ merchantId: unset
+ publicKey: unset
+ privateKey: secret://braintree.privateKey
+ environment: sandbox
+ graphqlUrl: unset
+ merchantAccounts:
+ # ISO 4217 currency code and its corresponding sub-merchant account
+ 'xts': unset
+ supportedCurrenciesByPaymentMethod:
+ PAYPAL:
+ - usd
+ pubSubPublisher:
+ type: stub
+
+dynamoDbClient:
+ type: local
+
+dynamoDbTables:
+ accounts:
+ tableName: accounts_test
+ phoneNumberTableName: numbers_test
+ phoneNumberIdentifierTableName: pni_assignment_test
+ usernamesTableName: usernames_test
+ backups:
+ tableName: backups_test
+ clientReleases:
+ tableName: client_releases_test
+ deletedAccounts:
+ tableName: deleted_accounts_test
+ deletedAccountsLock:
+ tableName: deleted_accounts_lock_test
+ issuedReceipts:
+ tableName: issued_receipts_test
+ expiration: P30D # Duration of time until rows expire
+ generator: abcdefg12345678= # random base64-encoded binary sequence
+ ecKeys:
+ tableName: keys_test
+ ecSignedPreKeys:
+ tableName: repeated_use_signed_ec_pre_keys_test
+ pqKeys:
+ tableName: pq_keys_test
+ pqLastResortKeys:
+ tableName: repeated_use_signed_kem_pre_keys_test
+ messages:
+ tableName: messages_test
+ expiration: P30D # Duration of time until rows expire
+ onetimeDonations:
+ tableName: onetime_donations_test
+ expiration: P90D
+ phoneNumberIdentifiers:
+ tableName: pni_test
+ profiles:
+ tableName: profiles_test
+ pushChallenge:
+ tableName: push_challenge_test
+ redeemedReceipts:
+ tableName: redeemed_receipts_test
+ expiration: P30D # Duration of time until rows expire
+ registrationRecovery:
+ tableName: registration_recovery_passwords_test
+ expiration: P300D # Duration of time until rows expire
+ remoteConfig:
+ tableName: remote_config_test
+ reportMessage:
+ tableName: report_messages_test
+ subscriptions:
+ tableName: subscriptions_test
+ clientPublicKeys:
+ tableName: client_public_keys_test
+ verificationSessions:
+ tableName: verification_sessions_test
+
+cacheCluster: # Redis server configuration for cache cluster
+ type: local
+
+clientPresenceCluster: # Redis server configuration for client presence cluster
+ type: local
+
+provisioning:
+ pubsub: # Redis server configuration for pubsub cluster
+ type: local
+
+pushSchedulerCluster: # Redis server configuration for push scheduler cluster
+ type: local
+
+rateLimitersCluster: # Redis server configuration for rate limiters cluster
+ type: local
+
+directoryV2:
+ client: # Configuration for interfacing with Contact Discovery Service v2 cluster
+ userAuthenticationTokenSharedSecret: secret://directoryV2.client.userAuthenticationTokenSharedSecret
+ userIdTokenSharedSecret: secret://directoryV2.client.userIdTokenSharedSecret
+
+svr2:
+ uri: svr2.example.com
+ userAuthenticationTokenSharedSecret: secret://svr2.userAuthenticationTokenSharedSecret
+ userIdTokenSharedSecret: secret://svr2.userIdTokenSharedSecret
+ svrCaCertificates:
+ # this is a randomly generated test certificate
+ - |
+ -----BEGIN CERTIFICATE-----
+ MIIDazCCAlOgAwIBAgIUW5lcNWkuynRVc8Rq5pO6mHQBuZAwDQYJKoZIhvcNAQEL
+ BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+ GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAzMjUwMzE4MTNaFw0yOTAz
+ MjQwMzE4MTNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
+ HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
+ AQUAA4IBDwAwggEKAoIBAQCfH4Um+fv2r4KudhD37/UXp8duRLTmp4XvpBTpDHpD
+ 2HF8p2yThVKlJnMkP/9Ey1Rb0vhxO7DCltLdW8IYcxJuHoyMvyhGUEtxxkOZbrk8
+ ciUR9jTZ37x7vXRGj/RxcdlS6iD0MeF0D/LAkImt4T/kiKwDbENrVEnYWJmipCKP
+ ribxWky7HqxDCoYMQr0zatxB3A9mx5stH+H3kbw3CZcm+ugF9ZIKDEVHb0lf28gq
+ llmD120q/vs9YV3rzVL7sBGDqf6olkulvHQJKElZg2rdcHWFcngSlU2BjR04oyuH
+ c/SSiLSB3YB0tdFGta5uorXyV1y7RElPeBfOfvEjsG3TAgMBAAGjUzBRMB0GA1Ud
+ DgQWBBQX+xlgSWWbDjv0SrJ+h67xauJ80zAfBgNVHSMEGDAWgBQX+xlgSWWbDjv0
+ SrJ+h67xauJ80zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAw
+ ZG2MCCjscn6h/QOoJU+IDfa68OqLq0I37gMnLMde4yEhAmm//miePIq4Uz9GRJ+h
+ rAmdEnspKgyQ93PjF7Xpk/JdJA4B1bIrsOl/cSwqx2sFhRt8Kt1DHGlGWXqOaHRP
+ UkZ86MyRL3sXly6WkxEYxZJeQaOzMy2XmQh7grzrlTBuSI+0xf7vsRRDipxr6LVQ
+ 6qGWyGODLLc2JD1IXj/1HpRVT2LoGGlKMuyxACQAm4oak1vvJ9mGxgfd9AU+eo58
+ O/esB2Eaf+QqMPELdFSZQfG2jvp+3WQTZK8fDKHyLr076G3UetEMy867F6fzTSZd
+ 9Kxq0DY7RCEpdHMCKcOL
+ -----END CERTIFICATE-----
+
+svr3:
+ uri: svr3.example.com
+ userAuthenticationTokenSharedSecret: secret://svr3.userAuthenticationTokenSharedSecret
+ userIdTokenSharedSecret: secret://svr3.userIdTokenSharedSecret
+ svrCaCertificates:
+ - |
+ -----BEGIN CERTIFICATE-----
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
+ AAAAAAAAAAAAAAAAAAAA
+ -----END CERTIFICATE-----
+
+
+messageCache: # Redis server configuration for message store cache
+ persistDelayMinutes: 1
+ cluster:
+ type: local
+
+metricsCluster:
+ type: local
+
+awsAttachments: # AWS S3 configuration
+ bucket: aws-attachments
+ credentials:
+ accessKeyId: secret://awsAttachments.accessKey
+ secretAccessKey: secret://awsAttachments.accessSecret
+ region: us-west-2
+
+gcpAttachments: # GCP Storage configuration
+ domain: example.com
+ email: user@example.cocm
+ maxSizeInBytes: 1024
+ pathPrefix:
+ rsaSigningKey: secret://gcpAttachments.rsaSigningKey
+
+tus:
+ uploadUri: https://example.org/upload
+ userAuthenticationTokenSharedSecret: secret://tus.userAuthenticationTokenSharedSecret
+
+apn: # Apple Push Notifications configuration
+ sandbox: true
+ bundleId: com.example.textsecuregcm
+ keyId: secret://apn.keyId
+ teamId: secret://apn.teamId
+ signingKey: secret://apn.signingKey
+
+fcm: # FCM configuration
+ credentials: secret://fcm.credentials
+
+cdn:
+ bucket: cdn # S3 Bucket name
+ credentials:
+ accessKeyId: secret://cdn.accessKey
+ secretAccessKey: secret://cdn.accessSecret
+ region: us-west-2 # AWS region
+
+clientCdn:
+ attachmentUrls:
+ 2: https://cdn2.example.com/attachments/
+ caCertificates:
+ - |
+ -----BEGIN CERTIFICATE-----
+ MIIDazCCAlOgAwIBAgIUW5lcNWkuynRVc8Rq5pO6mHQBuZAwDQYJKoZIhvcNAQEL
+ BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+ GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAzMjUwMzE4MTNaFw0yOTAz
+ MjQwMzE4MTNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
+ HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
+ AQUAA4IBDwAwggEKAoIBAQCfH4Um+fv2r4KudhD37/UXp8duRLTmp4XvpBTpDHpD
+ 2HF8p2yThVKlJnMkP/9Ey1Rb0vhxO7DCltLdW8IYcxJuHoyMvyhGUEtxxkOZbrk8
+ ciUR9jTZ37x7vXRGj/RxcdlS6iD0MeF0D/LAkImt4T/kiKwDbENrVEnYWJmipCKP
+ ribxWky7HqxDCoYMQr0zatxB3A9mx5stH+H3kbw3CZcm+ugF9ZIKDEVHb0lf28gq
+ llmD120q/vs9YV3rzVL7sBGDqf6olkulvHQJKElZg2rdcHWFcngSlU2BjR04oyuH
+ c/SSiLSB3YB0tdFGta5uorXyV1y7RElPeBfOfvEjsG3TAgMBAAGjUzBRMB0GA1Ud
+ DgQWBBQX+xlgSWWbDjv0SrJ+h67xauJ80zAfBgNVHSMEGDAWgBQX+xlgSWWbDjv0
+ SrJ+h67xauJ80zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAw
+ ZG2MCCjscn6h/QOoJU+IDfa68OqLq0I37gMnLMde4yEhAmm//miePIq4Uz9GRJ+h
+ rAmdEnspKgyQ93PjF7Xpk/JdJA4B1bIrsOl/cSwqx2sFhRt8Kt1DHGlGWXqOaHRP
+ UkZ86MyRL3sXly6WkxEYxZJeQaOzMy2XmQh7grzrlTBuSI+0xf7vsRRDipxr6LVQ
+ 6qGWyGODLLc2JD1IXj/1HpRVT2LoGGlKMuyxACQAm4oak1vvJ9mGxgfd9AU+eo58
+ O/esB2Eaf+QqMPELdFSZQfG2jvp+3WQTZK8fDKHyLr076G3UetEMy867F6fzTSZd
+ 9Kxq0DY7RCEpdHMCKcOL
+ -----END CERTIFICATE-----
+
+cdn3StorageManager:
+ baseUri: https://storage-manager.example.com
+ clientId: example
+ clientSecret: secret://cdn3StorageManager.clientSecret
+
+dogstatsd:
+ type: nowait
+ environment: dev
+ host: 127.0.0.1
+
+unidentifiedDelivery:
+ certificate: secret://unidentifiedDelivery.certificate
+ privateKey: secret://unidentifiedDelivery.privateKey
+ expiresDays: 7
+
+hCaptcha:
+ type: stub
+
+shortCode:
+ baseUrl: https://example.com/shortcodes/
+
+storageService:
+ uri: storage.example.com
+ userAuthenticationTokenSharedSecret: secret://storageService.userAuthenticationTokenSharedSecret
+ storageCaCertificates:
+ - |
+ -----BEGIN CERTIFICATE-----
+ MIIDazCCAlOgAwIBAgIUW5lcNWkuynRVc8Rq5pO6mHQBuZAwDQYJKoZIhvcNAQEL
+ BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+ GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAzMjUwMzE4MTNaFw0yOTAz
+ MjQwMzE4MTNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
+ HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
+ AQUAA4IBDwAwggEKAoIBAQCfH4Um+fv2r4KudhD37/UXp8duRLTmp4XvpBTpDHpD
+ 2HF8p2yThVKlJnMkP/9Ey1Rb0vhxO7DCltLdW8IYcxJuHoyMvyhGUEtxxkOZbrk8
+ ciUR9jTZ37x7vXRGj/RxcdlS6iD0MeF0D/LAkImt4T/kiKwDbENrVEnYWJmipCKP
+ ribxWky7HqxDCoYMQr0zatxB3A9mx5stH+H3kbw3CZcm+ugF9ZIKDEVHb0lf28gq
+ llmD120q/vs9YV3rzVL7sBGDqf6olkulvHQJKElZg2rdcHWFcngSlU2BjR04oyuH
+ c/SSiLSB3YB0tdFGta5uorXyV1y7RElPeBfOfvEjsG3TAgMBAAGjUzBRMB0GA1Ud
+ DgQWBBQX+xlgSWWbDjv0SrJ+h67xauJ80zAfBgNVHSMEGDAWgBQX+xlgSWWbDjv0
+ SrJ+h67xauJ80zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAw
+ ZG2MCCjscn6h/QOoJU+IDfa68OqLq0I37gMnLMde4yEhAmm//miePIq4Uz9GRJ+h
+ rAmdEnspKgyQ93PjF7Xpk/JdJA4B1bIrsOl/cSwqx2sFhRt8Kt1DHGlGWXqOaHRP
+ UkZ86MyRL3sXly6WkxEYxZJeQaOzMy2XmQh7grzrlTBuSI+0xf7vsRRDipxr6LVQ
+ 6qGWyGODLLc2JD1IXj/1HpRVT2LoGGlKMuyxACQAm4oak1vvJ9mGxgfd9AU+eo58
+ O/esB2Eaf+QqMPELdFSZQfG2jvp+3WQTZK8fDKHyLr076G3UetEMy867F6fzTSZd
+ 9Kxq0DY7RCEpdHMCKcOL
+ -----END CERTIFICATE-----
+
+zkConfig:
+ serverPublic: AAp8oB0D4EV2q7hSue3Kxzh1Vc88/nmLuRR9G3EefC0+CMcxJFQwDMgjFvFBKx3o6m9gJLevYiKcm/NxXX9WtnFMDHgDgfqHxbCi2rm20SgoHnuoph6XArmEOX6a1xLJVxgDtgfm1IbcyyqROXYxe9v2RvMUAnjbLI/fm0rXXhldjszlVR/wRpybX90RUjFyL/2Achttf3IC/ShWKkB6mWXwuFCcNfzeCCQ+w7cNnDbWscBcrhuou7HZvbt16/YdCXLyp+WdwS8ZkelpITvyK2hsPvf4oxaRLQfVRYXUMX55xpapbKH6PthuOzMVRkf+I3Xz3/bNjiQSlQkmAXlgB1YujgABYnJ6yJXQKP2mR4UJ3UYoGroYoafWycDa+vUYYozaUmzFjsBYWpYE+HyPJlJ2QaFTrpVqxX7NXsSbg8t35IvfWfZME9YBZ2eErDunwkaE4iDQhHl5IXAhbHDrr2QaJ68YIkn7lJSgFDKGFB2kb6BvDUGzcpI/CTHQi6WlCqQidQLJWDFFdlYjrUCQM2vvJtgyGrSc89jdXTFjM31aqmtcPWgWL0qv+RmK/BC392Nsu8WoSJcAE4yhccQuRSemtolgwewnjasoOFBNOPh4+pX55SwhyTVgtwl+NTNVNFydxGp9Me8ogRWElzwA9BFtNAgQtlfgIyZRTetFqLkYmIBDxwMcpizDKES5lPhV2uJJuzcMq/06mVQz2OrXgglWk01uN8U59pfNFpTZhcGQv+MHjwEAudq5eLpt3aFrdxJ7D26Fwl5j215SJ0yZo7vmSEML1vf7FaGh0IL57bRpCvdebB5WapSChUX+PPvCXohVjGrERFvQpeET6pydGGlEKYLWuWa3zFGmPvJJYZ/QfcmIP9zyhqzQT/7a7RIqFA==
+ serverSecret: secret://zkConfig-libsignal-0.42.serverSecret
+
+callingZkConfig:
+ serverSecret: secret://callingZkConfig.serverSecret
+
+backupsZkConfig:
+ serverSecret: secret://backupsZkConfig.serverSecret
+
+appConfig:
+ type: static
+ application: test
+ environment: test
+ configuration: test
+ staticConfig: |
+ captcha:
+ scoreFloor: 1.0
+
+remoteConfig:
+ globalConfig: # keys and values that are given to clients on GET /v1/config
+ EXAMPLE_KEY: VALUE
+
+paymentsService:
+ userAuthenticationTokenSharedSecret: secret://paymentsService.userAuthenticationTokenSharedSecret
+ paymentCurrencies:
+ # list of symbols for supported currencies
+ - MOB
+ externalClients:
+ type: stub
+
+artService:
+ userAuthenticationTokenSharedSecret: secret://artService.userAuthenticationTokenSharedSecret
+ userAuthenticationTokenUserIdSecret: secret://artService.userAuthenticationTokenUserIdSecret
+
+badges:
+ badges:
+ - id: TEST
+ category: other
+ sprites: # exactly 6
+ - sprite-1.png
+ - sprite-2.png
+ - sprite-3.png
+ - sprite-4.png
+ - sprite-5.png
+ - sprite-6.png
+ svg: example.svg
+ svgs:
+ - light: example-light.svg
+ dark: example-dark.svg
+ badgeIdsEnabledForAll:
+ - TEST
+ receiptLevels:
+ '1': TEST
+
+subscription: # configuration for Stripe subscriptions
+ badgeExpiration: P30D
+ badgeGracePeriod: P15D
+ levels:
+ 500:
+ badge: EXAMPLE
+ prices:
+ # list of ISO 4217 currency codes and amounts for the given badge level
+ xts:
+ amount: '10'
+ processorIds:
+ STRIPE: price_example # stripe Price ID
+ BRAINTREE: plan_example # braintree Plan ID
+
+oneTimeDonations:
+ sepaMaximumEuros: '10000'
+ boost:
+ level: 1
+ expiration: P90D
+ badge: EXAMPLE
+ gift:
+ level: 10
+ expiration: P90D
+ badge: EXAMPLE
+ currencies:
+ # ISO 4217 currency codes and amounts in those currencies
+ xts:
+ minimum: '0.5'
+ gift: '2'
+ boosts:
+ - '1'
+ - '2'
+ - '4'
+ - '8'
+ - '20'
+ - '40'
+
+registrationService:
+ type: stub
+ registrationCaCertificate: |
+ -----BEGIN CERTIFICATE-----
+ MIIDazCCAlOgAwIBAgIUW5lcNWkuynRVc8Rq5pO6mHQBuZAwDQYJKoZIhvcNAQEL
+ BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+ GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAzMjUwMzE4MTNaFw0yOTAz
+ MjQwMzE4MTNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
+ HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
+ AQUAA4IBDwAwggEKAoIBAQCfH4Um+fv2r4KudhD37/UXp8duRLTmp4XvpBTpDHpD
+ 2HF8p2yThVKlJnMkP/9Ey1Rb0vhxO7DCltLdW8IYcxJuHoyMvyhGUEtxxkOZbrk8
+ ciUR9jTZ37x7vXRGj/RxcdlS6iD0MeF0D/LAkImt4T/kiKwDbENrVEnYWJmipCKP
+ ribxWky7HqxDCoYMQr0zatxB3A9mx5stH+H3kbw3CZcm+ugF9ZIKDEVHb0lf28gq
+ llmD120q/vs9YV3rzVL7sBGDqf6olkulvHQJKElZg2rdcHWFcngSlU2BjR04oyuH
+ c/SSiLSB3YB0tdFGta5uorXyV1y7RElPeBfOfvEjsG3TAgMBAAGjUzBRMB0GA1Ud
+ DgQWBBQX+xlgSWWbDjv0SrJ+h67xauJ80zAfBgNVHSMEGDAWgBQX+xlgSWWbDjv0
+ SrJ+h67xauJ80zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAw
+ ZG2MCCjscn6h/QOoJU+IDfa68OqLq0I37gMnLMde4yEhAmm//miePIq4Uz9GRJ+h
+ rAmdEnspKgyQ93PjF7Xpk/JdJA4B1bIrsOl/cSwqx2sFhRt8Kt1DHGlGWXqOaHRP
+ UkZ86MyRL3sXly6WkxEYxZJeQaOzMy2XmQh7grzrlTBuSI+0xf7vsRRDipxr6LVQ
+ 6qGWyGODLLc2JD1IXj/1HpRVT2LoGGlKMuyxACQAm4oak1vvJ9mGxgfd9AU+eo58
+ O/esB2Eaf+QqMPELdFSZQfG2jvp+3WQTZK8fDKHyLr076G3UetEMy867F6fzTSZd
+ 9Kxq0DY7RCEpdHMCKcOL
+ -----END CERTIFICATE-----
+
+turn:
+ secret: secret://turn.secret
+
+linkDevice:
+ secret: secret://linkDevice.secret
+
+maxmindCityDatabase:
+ type: static
+
+callingTurnDnsRecords:
+ type: static
+
+callingTurnPerformanceTable:
+ type: static
+
+callingTurnManualTable:
+ type: static
+
+noiseTunnel:
+ port: 8443
+ recognizedProxySecret: secret://noiseTunnel.recognizedProxySecret
+
+externalRequestFilter:
+ grpcMethods:
+ - com.example.grpc.ExampleService/exampleMethod
+ paths:
+ - /example
+ permittedInternalRanges:
+ - 127.0.0.0/8