Add tests for WhisperServerService#run

Additionally, `LocalWhisperServerService` may be used for integration testing.
This commit is contained in:
Chris Eager
2024-04-29 11:05:35 -05:00
committed by GitHub
parent b6f8bca361
commit 0e4be0c85a
84 changed files with 2156 additions and 552 deletions

View File

@@ -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());
}
}

View File

@@ -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());
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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))));
}
}
}

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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());
}

View File

@@ -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);
}
}