Use DynamoDB for change number waiting periods
DynamoDB is even simpler for integration tests.
This commit is contained in:
parent
23305e4460
commit
460e5cb499
@ -5,20 +5,14 @@
|
||||
|
||||
package org.signal.integration;
|
||||
|
||||
import io.lettuce.core.resource.ClientResources;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import org.signal.integration.config.Config;
|
||||
import org.whispersystems.textsecuregcm.metrics.NoopAwsSdkMetricPublisher;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClusterClient;
|
||||
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberWaitingPeriodManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberWaitingPeriods;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||
@ -28,6 +22,7 @@ import org.whispersystems.textsecuregcm.util.Util;
|
||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
|
||||
public class IntegrationTools {
|
||||
|
||||
@ -37,7 +32,7 @@ public class IntegrationTools {
|
||||
|
||||
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||
|
||||
private final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager;
|
||||
private final ChangeNumberWaitingPeriods changeNumberWaitingPeriods;
|
||||
|
||||
public static IntegrationTools create(final Config config) {
|
||||
final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.builder().build();
|
||||
@ -45,22 +40,20 @@ public class IntegrationTools {
|
||||
final DynamoDbAsyncClient dynamoDbAsyncClient =
|
||||
config.dynamoDbClient().buildAsyncClient(credentialsProvider, new NoopAwsSdkMetricPublisher());
|
||||
|
||||
final DynamoDbClient dynamoDbClient =
|
||||
config.dynamoDbClient().buildSyncClient(credentialsProvider, new NoopAwsSdkMetricPublisher());
|
||||
|
||||
final RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
|
||||
config.dynamoDbTables().registrationRecovery(), Duration.ofDays(1), dynamoDbAsyncClient, Clock.systemUTC());
|
||||
|
||||
final VerificationSessions verificationSessions = new VerificationSessions(
|
||||
dynamoDbAsyncClient, config.dynamoDbTables().verificationSessions(), Clock.systemUTC());
|
||||
|
||||
final FaultTolerantRedisClusterClient rateLimitersClient = new FaultTolerantRedisClusterClient(
|
||||
"rateLimiters",
|
||||
config.redis().rateLimiters(),
|
||||
ClientResources.builder());
|
||||
|
||||
return new IntegrationTools(
|
||||
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords),
|
||||
new VerificationSessionManager(verificationSessions),
|
||||
new PhoneNumberIdentifiers(dynamoDbAsyncClient, config.dynamoDbTables().phoneNumberIdentifiers()),
|
||||
new ChangeNumberWaitingPeriodManager(rateLimitersClient, Duration.ZERO)
|
||||
new ChangeNumberWaitingPeriods(config.dynamoDbTables().changeNumberWaitingPeriods(), dynamoDbAsyncClient, dynamoDbClient)
|
||||
);
|
||||
}
|
||||
|
||||
@ -68,11 +61,11 @@ public class IntegrationTools {
|
||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||
final VerificationSessionManager verificationSessionManager,
|
||||
final PhoneNumberIdentifiers phoneNumberIdentifiers,
|
||||
final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager) {
|
||||
final ChangeNumberWaitingPeriods changeNumberWaitingPeriods) {
|
||||
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||
this.verificationSessionManager = verificationSessionManager;
|
||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||
this.changeNumberWaitingPeriodManager = changeNumberWaitingPeriodManager;
|
||||
this.changeNumberWaitingPeriods = changeNumberWaitingPeriods;
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> populateRecoveryPassword(final String phoneNumber, final byte[] password) {
|
||||
@ -88,11 +81,6 @@ public class IntegrationTools {
|
||||
}
|
||||
|
||||
public void clearChangeNumberWaitingPeriod(TestUser user) {
|
||||
try {
|
||||
changeNumberWaitingPeriodManager.handleAccountCreated(user.aciUuid(), Instant.now().minus(Duration.ofDays(1)))
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
changeNumberWaitingPeriods.delete(user.aciUuid());
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,5 @@ public record Config(@NotBlank String domain,
|
||||
@NotNull @Valid DynamoDbClientFactory dynamoDbClient,
|
||||
@NotNull @Valid DynamoDbTables dynamoDbTables,
|
||||
@NotBlank String prescribedRegistrationNumber,
|
||||
@NotBlank String prescribedRegistrationCode,
|
||||
@NotNull @Valid Redis redis) {
|
||||
@NotBlank String prescribedRegistrationCode) {
|
||||
}
|
||||
|
||||
@ -9,5 +9,6 @@ import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record DynamoDbTables(@NotBlank String registrationRecovery,
|
||||
@NotBlank String verificationSessions,
|
||||
@NotBlank String phoneNumberIdentifiers) {
|
||||
@NotBlank String phoneNumberIdentifiers,
|
||||
@NotBlank String changeNumberWaitingPeriods) {
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration.config;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedisClusterConfiguration;
|
||||
|
||||
public record Redis(@NotNull @Valid RedisClusterConfiguration rateLimiters) {
|
||||
}
|
||||
@ -92,6 +92,8 @@ dynamoDbTables:
|
||||
tableName: Example_AppleDeviceCheckPublicKeys
|
||||
backups:
|
||||
tableName: Example_Backups
|
||||
changeNumberWaitingPeriods:
|
||||
tableName: Example_ChangeNumberWaitingPeriods
|
||||
clientReleases:
|
||||
tableName: Example_ClientReleases
|
||||
deletedAccounts:
|
||||
|
||||
@ -236,6 +236,7 @@ import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberWaitingPeriodManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberWaitingPeriods;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleases;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
@ -706,8 +707,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new RedisMessageAvailabilityManager(messagesCluster, clientEventExecutor, asyncOperationQueueingExecutor);
|
||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, redisMessageAvailabilityManager,
|
||||
reportMessageManager, messageDeletionAsyncExecutor, Clock.systemUTC());
|
||||
final ChangeNumberWaitingPeriods changeNumberWaitingPeriods = new ChangeNumberWaitingPeriods(
|
||||
config.getDynamoDbTables().getChangeNumberWaitingPeriods().getTableName(), dynamoDbAsyncClient, dynamoDbClient);
|
||||
final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager = new ChangeNumberWaitingPeriodManager(
|
||||
rateLimitersCluster, config.getChangeNumber().postRegistrationWaitingPeriod());
|
||||
changeNumberWaitingPeriods, config.getChangeNumber().postRegistrationWaitingPeriod(), clock);
|
||||
AccountLockManager accountLockManager = new AccountLockManager(dynamoDbClient,
|
||||
config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
|
||||
@ -49,6 +49,7 @@ public class DynamoDbTables {
|
||||
private final AccountsTableConfiguration accounts;
|
||||
|
||||
private final Table appleDeviceChecks;
|
||||
private final Table changeNumberWaitingPeriods;
|
||||
private final Table appleDeviceCheckPublicKeys;
|
||||
private final Table backups;
|
||||
private final Table clientPublicKeys;
|
||||
@ -78,6 +79,7 @@ public class DynamoDbTables {
|
||||
public DynamoDbTables(
|
||||
@JsonProperty("accounts") final AccountsTableConfiguration accounts,
|
||||
@JsonProperty("appleDeviceChecks") final Table appleDeviceChecks,
|
||||
@JsonProperty("changeNumberWaitingPeriods") final Table changeNumberWaitingPeriods,
|
||||
@JsonProperty("appleDeviceCheckPublicKeys") final Table appleDeviceCheckPublicKeys,
|
||||
@JsonProperty("backups") final Table backups,
|
||||
@JsonProperty("clientPublicKeys") final Table clientPublicKeys,
|
||||
@ -106,6 +108,7 @@ public class DynamoDbTables {
|
||||
|
||||
this.accounts = accounts;
|
||||
this.appleDeviceChecks = appleDeviceChecks;
|
||||
this.changeNumberWaitingPeriods = changeNumberWaitingPeriods;
|
||||
this.appleDeviceCheckPublicKeys = appleDeviceCheckPublicKeys;
|
||||
this.backups = backups;
|
||||
this.clientPublicKeys = clientPublicKeys;
|
||||
@ -145,6 +148,12 @@ public class DynamoDbTables {
|
||||
return appleDeviceChecks;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getChangeNumberWaitingPeriods() {
|
||||
return changeNumberWaitingPeriods;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
public Table getAppleDeviceCheckPublicKeys() {
|
||||
|
||||
@ -6,59 +6,39 @@
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.lettuce.core.SetArgs;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClusterClient;
|
||||
|
||||
/// Manages post-registration change number waiting period expiration data
|
||||
public class ChangeNumberWaitingPeriodManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ChangeNumberWaitingPeriodManager.class);
|
||||
|
||||
private final FaultTolerantRedisClusterClient redisCluster;
|
||||
private final ChangeNumberWaitingPeriods changeNumberWaitingPeriods;
|
||||
private final Duration waitingPeriod;
|
||||
private final Clock clock;
|
||||
|
||||
public ChangeNumberWaitingPeriodManager(final FaultTolerantRedisClusterClient redisCluster, final Duration waitingPeriod) {
|
||||
this.redisCluster = redisCluster;
|
||||
public ChangeNumberWaitingPeriodManager(final ChangeNumberWaitingPeriods changeNumberWaitingPeriods,
|
||||
final Duration waitingPeriod, final Clock clock) {
|
||||
this.changeNumberWaitingPeriods = changeNumberWaitingPeriods;
|
||||
this.waitingPeriod = waitingPeriod;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/// Must be called when an account is created, including re-registration
|
||||
@VisibleForTesting
|
||||
public CompletableFuture<Void> handleAccountCreated(final UUID aci, final Instant created) {
|
||||
return redisCluster.withCluster(conn -> conn.async().set(key(aci), "", SetArgs.Builder.exAt(created.plus(waitingPeriod))))
|
||||
.toCompletableFuture()
|
||||
.thenApply(_ -> null);
|
||||
return changeNumberWaitingPeriods.setExpiration(aci, created.plus(waitingPeriod));
|
||||
}
|
||||
|
||||
/// Returns the waiting period duration remaining, if any. If present, {@code duration} will always be positive.
|
||||
Optional<Duration> getWaitingPeriodRemaining(final UUID aci) {
|
||||
final long ttlMillis = redisCluster.withCluster(conn -> conn.sync().ttl(key(aci)));
|
||||
|
||||
if (ttlMillis == -1) {
|
||||
// key present without TTL. This should never happen.
|
||||
LOGGER.error("No expiration for {}", aci);
|
||||
throw new RuntimeException("No expiration for key that must always have a expiration");
|
||||
}
|
||||
|
||||
if (ttlMillis == -2) {
|
||||
// key did not exist
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final Duration remaining = Duration.ofMillis(ttlMillis);
|
||||
|
||||
return remaining.isPositive() ? Optional.of(remaining) : Optional.empty();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static String key(final UUID aci) {
|
||||
return "changeNumberWaiting::{" + aci + "}";
|
||||
return changeNumberWaitingPeriods.getExpiration(aci)
|
||||
.flatMap(expiration -> {
|
||||
final Duration remaining = Duration.between(clock.instant(), expiration);
|
||||
return remaining.isPositive() ? Optional.of(remaining) : Optional.empty();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
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;
|
||||
|
||||
public class ChangeNumberWaitingPeriods {
|
||||
|
||||
// hash key; bytes
|
||||
static final String KEY_ACCOUNT_UUID = "U";
|
||||
// expiration timestamp in epoch seconds; number
|
||||
static final String ATTR_TTL = "E";
|
||||
|
||||
private final String tableName;
|
||||
private final DynamoDbAsyncClient dynamoDbAsyncClient;
|
||||
private final DynamoDbClient dynamoDbClient;
|
||||
|
||||
public ChangeNumberWaitingPeriods(final String tableName, final DynamoDbAsyncClient dynamoDbAsyncClient, final DynamoDbClient dynamoDbClient) {
|
||||
this.tableName = tableName;
|
||||
this.dynamoDbAsyncClient = dynamoDbAsyncClient;
|
||||
this.dynamoDbClient = dynamoDbClient;
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> setExpiration(final UUID aci, final Instant expiration) {
|
||||
return dynamoDbAsyncClient.putItem(PutItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.item(Map.of(
|
||||
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(aci),
|
||||
ATTR_TTL, AttributeValues.fromLong(expiration.getEpochSecond())))
|
||||
.build())
|
||||
.thenRun(Util.NOOP);
|
||||
}
|
||||
|
||||
public Optional<Instant> getExpiration(final UUID aci) {
|
||||
final GetItemResponse response = dynamoDbClient.getItem(GetItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(aci)))
|
||||
.consistentRead(true)
|
||||
.build());
|
||||
if (!response.hasItem()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return AttributeValues.get(response.item(), ATTR_TTL)
|
||||
.map(AttributeValue::n)
|
||||
.map(Long::parseLong)
|
||||
.map(Instant::ofEpochSecond)
|
||||
.filter(instant -> instant.isAfter(Instant.now()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void delete(final UUID aci) {
|
||||
dynamoDbClient.deleteItem(DeleteItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(aci)))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@ -53,6 +53,7 @@ import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberWaitingPeriodManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ChangeNumberWaitingPeriods;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamoDbRecoveryManager;
|
||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||
@ -282,8 +283,10 @@ public record CommandDependencies(
|
||||
configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||
RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
|
||||
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
|
||||
final ChangeNumberWaitingPeriods changeNumberWaitingPeriods = new ChangeNumberWaitingPeriods(
|
||||
configuration.getDynamoDbTables().getChangeNumberWaitingPeriods().getTableName(), dynamoDbAsyncClient, dynamoDbClient);
|
||||
final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager = new ChangeNumberWaitingPeriodManager(
|
||||
rateLimitersCluster, configuration.getChangeNumber().postRegistrationWaitingPeriod());
|
||||
changeNumberWaitingPeriods, configuration.getChangeNumber().postRegistrationWaitingPeriod(), clock);
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
pubsubClient, accountLockManager, keys, messagesManager, profilesManager,
|
||||
changeNumberWaitingPeriodManager, secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager,
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
@ -15,12 +15,13 @@ import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
|
||||
|
||||
class ChangeNumberWaitingPeriodManagerTest {
|
||||
|
||||
@RegisterExtension
|
||||
static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
|
||||
static final DynamoDbExtension DYNAMO_DB_EXTENSION =
|
||||
new DynamoDbExtension(Tables.CHANGE_NUMBER_WAITING_PERIODS);
|
||||
|
||||
private static final Duration WAITING_PERIOD = Duration.ofDays(7);
|
||||
|
||||
@ -29,7 +30,11 @@ class ChangeNumberWaitingPeriodManagerTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
changeNumberWaitingPeriodManager = new ChangeNumberWaitingPeriodManager(
|
||||
REDIS_CLUSTER_EXTENSION.getRedisCluster(), WAITING_PERIOD);
|
||||
new ChangeNumberWaitingPeriods(Tables.CHANGE_NUMBER_WAITING_PERIODS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient()),
|
||||
WAITING_PERIOD,
|
||||
Clock.systemUTC());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -54,17 +59,4 @@ class ChangeNumberWaitingPeriodManagerTest {
|
||||
|
||||
assertTrue(changeNumberWaitingPeriodManager.getWaitingPeriodRemaining(aci).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoTtlException() throws Exception {
|
||||
final UUID aci = UUID.randomUUID();
|
||||
|
||||
changeNumberWaitingPeriodManager.handleAccountCreated(aci, Instant.now()).get(5, TimeUnit.SECONDS);
|
||||
|
||||
REDIS_CLUSTER_EXTENSION.getRedisCluster().useCluster(conn ->
|
||||
conn.sync().persist(ChangeNumberWaitingPeriodManager.key(aci)));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> changeNumberWaitingPeriodManager.getWaitingPeriodRemaining(aci),
|
||||
"This is an impossible scenario, and it should throw an exception");
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
|
||||
|
||||
class ChangeNumberWaitingPeriodsTest {
|
||||
|
||||
@RegisterExtension
|
||||
static final DynamoDbExtension DYNAMO_DB_EXTENSION =
|
||||
new DynamoDbExtension(Tables.CHANGE_NUMBER_WAITING_PERIODS);
|
||||
|
||||
private ChangeNumberWaitingPeriods changeNumberWaitingPeriods;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
changeNumberWaitingPeriods = new ChangeNumberWaitingPeriods(
|
||||
Tables.CHANGE_NUMBER_WAITING_PERIODS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbClient());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getExpiration_unknownAci() throws Exception {
|
||||
assertTrue(changeNumberWaitingPeriods.getExpiration(UUID.randomUUID()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void setAndGetExpiration() throws Exception {
|
||||
final UUID aci = UUID.randomUUID();
|
||||
// truncate to seconds because the TTL attribute stores epoch seconds
|
||||
final Instant expiration = Instant.now().plusSeconds(3600).truncatedTo(ChronoUnit.SECONDS);
|
||||
|
||||
changeNumberWaitingPeriods.setExpiration(aci, expiration).get();
|
||||
|
||||
final Optional<Instant> result = changeNumberWaitingPeriods.getExpiration(aci);
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals(expiration, result.get());
|
||||
|
||||
assertTrue(changeNumberWaitingPeriods.getExpiration(UUID.randomUUID()).isEmpty(), "new UUID has no entry");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setExpiration_overwritesExistingEntry() throws Exception {
|
||||
final UUID aci = UUID.randomUUID();
|
||||
final Instant first = Instant.now().plusSeconds(3600).truncatedTo(ChronoUnit.SECONDS);
|
||||
final Instant second = Instant.now().plusSeconds(7200).truncatedTo(ChronoUnit.SECONDS);
|
||||
|
||||
changeNumberWaitingPeriods.setExpiration(aci, first).get();
|
||||
|
||||
{
|
||||
final Optional<Instant> result = changeNumberWaitingPeriods.getExpiration(aci);
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals(first, result.get());
|
||||
}
|
||||
|
||||
changeNumberWaitingPeriods.setExpiration(aci, second).get();
|
||||
|
||||
{
|
||||
final Optional<Instant> result = changeNumberWaitingPeriods.getExpiration(aci);
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals(second, result.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void delete_removesExisting() throws Exception {
|
||||
final UUID aci = UUID.randomUUID();
|
||||
changeNumberWaitingPeriods.setExpiration(aci, Instant.now().plusSeconds(3600)).get();
|
||||
|
||||
assertTrue( changeNumberWaitingPeriods.getExpiration(aci).isPresent());
|
||||
|
||||
changeNumberWaitingPeriods.delete(aci);
|
||||
|
||||
assertTrue(changeNumberWaitingPeriods.getExpiration(aci).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void delete_missingIsNoop() {
|
||||
assertDoesNotThrow(() -> changeNumberWaitingPeriods.delete(UUID.randomUUID()));
|
||||
}
|
||||
}
|
||||
@ -60,6 +60,15 @@ public final class DynamoDbExtensionSchema {
|
||||
.attributeType(ScalarAttributeType.B).build()),
|
||||
Collections.emptyList(), Collections.emptyList()),
|
||||
|
||||
CHANGE_NUMBER_WAITING_PERIODS("change_number_waiting_periods_test",
|
||||
ChangeNumberWaitingPeriods.KEY_ACCOUNT_UUID,
|
||||
null,
|
||||
List.of(AttributeDefinition.builder()
|
||||
.attributeName(ChangeNumberWaitingPeriods.KEY_ACCOUNT_UUID)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build()),
|
||||
List.of(), List.of()),
|
||||
|
||||
CLIENT_RELEASES("client_releases_test",
|
||||
ClientReleases.ATTR_PLATFORM,
|
||||
ClientReleases.ATTR_VERSION,
|
||||
|
||||
@ -98,6 +98,8 @@ dynamoDbTables:
|
||||
tableName: apple_device_check_public_keys_test
|
||||
backups:
|
||||
tableName: backups_test
|
||||
changeNumberWaitingPeriods:
|
||||
tableName: change_number_waiting_periods_test
|
||||
clientReleases:
|
||||
tableName: client_releases_test
|
||||
deletedAccounts:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user