mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 13:28:08 +01:00
Add tests for WhisperServerService#run
Additionally, `LocalWhisperServerService` may be used for integration testing.
This commit is contained in:
@@ -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:
|
||||
* <p>
|
||||
* <code>./mvnw clean integration-test -DskipTests=true -Ptest-server</code>
|
||||
* <p>
|
||||
* <strong>NOTE: many features are non-functional, especially those that depend on external services</strong>
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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<WhisperServerConfiguration> 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 <T> DynamicConfigurationManager<T> build(final Class<T> klazz,
|
||||
final ScheduledExecutorService scheduledExecutorService, final AwsCredentialsProvider awsCredentialsProvider) {
|
||||
|
||||
return new StaticDynamicConfigurationManager<>(staticConfig, application, environment, configuration,
|
||||
awsCredentialsProvider, klazz, scheduledExecutorService);
|
||||
}
|
||||
|
||||
private static class StaticDynamicConfigurationManager<T> extends DynamicConfigurationManager<T> {
|
||||
|
||||
private final T configuration;
|
||||
|
||||
public StaticDynamicConfigurationManager(final String config, final String application, final String environment,
|
||||
final String configurationName, final AwsCredentialsProvider awsCredentialsProvider,
|
||||
final Class<T> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<InputStream> changeListener) {
|
||||
changeListener.accept(new ByteArrayInputStream(object));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<DynamicConfiguration> 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<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
super(null, retryExecutor, circuitBreakerConfiguration, null, dynamicConfigurationManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String scheme() {
|
||||
return "test";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, BigDecimal> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<String, RegistrationServiceSession> SESSIONS = new ConcurrentHashMap<>();
|
||||
|
||||
public StubRegistrationServiceClient(final String registrationCaCertificate) throws IOException {
|
||||
super("example.com", 8080, null, registrationCaCertificate, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<RegistrationServiceSession> 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<RegistrationServiceSession> 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<RegistrationServiceSession> 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<Optional<RegistrationServiceSession>> getSession(final byte[] sessionId,
|
||||
final Duration timeout) {
|
||||
return CompletableFuture.completedFuture(
|
||||
Optional.ofNullable(SESSIONS.get(Base64.getEncoder().encodeToString(sessionId))));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, String> 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);
|
||||
|
||||
@@ -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<GetObjectResponse> 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<GetObjectResponse> 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());
|
||||
}
|
||||
|
||||
@@ -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<TableSchema> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user