Refactor Change Number waiting period to dedicated manager
This simplifies integration testing.
This commit is contained in:
parent
9dc6e049f4
commit
dd29ee1f27
@ -235,6 +235,7 @@ import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
||||
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.ClientReleaseManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ClientReleases;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
@ -705,11 +706,13 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new RedisMessageAvailabilityManager(messagesCluster, clientEventExecutor, asyncOperationQueueingExecutor);
|
||||
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, redisMessageAvailabilityManager,
|
||||
reportMessageManager, messageDeletionAsyncExecutor, Clock.systemUTC());
|
||||
final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager = new ChangeNumberWaitingPeriodManager(
|
||||
rateLimitersCluster, config.getChangeNumber().postRegistrationWaitingPeriod());
|
||||
AccountLockManager accountLockManager = new AccountLockManager(dynamoDbClient,
|
||||
config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
pubsubClient, accountLockManager, keysManager, messagesManager, profilesManager,
|
||||
secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager,
|
||||
changeNumberWaitingPeriodManager, secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager,
|
||||
registrationRecoveryPasswordsManager, accountLockExecutor, messagePollExecutor,
|
||||
retryExecutor, clock, config.getLinkDeviceSecretConfiguration().secret().value());
|
||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
@ -1118,7 +1121,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
final ChangeNumberManager changeNumberManager = new ChangeNumberManager(messageSender, accountsManager,
|
||||
phoneVerificationTokenManager, registrationLockVerificationManager, rateLimiters,
|
||||
config.getChangeNumber().postRegistrationWaitingPeriod(), Clock.systemUTC());
|
||||
changeNumberWaitingPeriodManager, Clock.systemUTC());
|
||||
|
||||
final List<Object> commonControllers = Lists.newArrayList(
|
||||
new AccountController(accountsManager, rateLimiters, registrationRecoveryPasswordsManager,
|
||||
|
||||
@ -129,6 +129,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
private final KeysManager keysManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager;
|
||||
private final SecureStorageClient secureStorageClient;
|
||||
private final SecureValueRecoveryClient secureValueRecovery2Client;
|
||||
private final DisconnectionRequestManager disconnectionRequestManager;
|
||||
@ -210,7 +211,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
final AccountLockManager accountLockManager,
|
||||
final KeysManager keysManager,
|
||||
final MessagesManager messagesManager,
|
||||
final ProfilesManager profilesManager,
|
||||
final ProfilesManager profilesManager, final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager,
|
||||
final SecureStorageClient secureStorageClient,
|
||||
final SecureValueRecoveryClient secureValueRecovery2Client,
|
||||
final DisconnectionRequestManager disconnectionRequestManager,
|
||||
@ -227,6 +228,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
this.keysManager = keysManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.profilesManager = profilesManager;
|
||||
this.changeNumberWaitingPeriodManager = changeNumberWaitingPeriodManager;
|
||||
this.secureStorageClient = secureStorageClient;
|
||||
this.secureValueRecovery2Client = secureValueRecovery2Client;
|
||||
this.disconnectionRequestManager = disconnectionRequestManager;
|
||||
@ -403,7 +405,8 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
return CompletableFuture.allOf(keysManager.deleteSingleUsePreKeys(aci),
|
||||
keysManager.deleteSingleUsePreKeys(pni),
|
||||
messagesManager.clear(aci),
|
||||
profilesManager.deleteAll(aci, false));
|
||||
profilesManager.deleteAll(aci, false),
|
||||
changeNumberWaitingPeriodManager.handleAccountCreated(aci, clock.instant()));
|
||||
})
|
||||
.join();
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import io.micrometer.core.instrument.Tags;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -48,7 +47,7 @@ public class ChangeNumberManager {
|
||||
private final AccountsManager accountsManager;
|
||||
private final PhoneVerificationTokenManager phoneVerificationTokenManager;
|
||||
private final RegistrationLockVerificationManager registrationLockVerificationManager;
|
||||
private final Duration postRegistrationWaitingPeriod;
|
||||
private final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Clock clock;
|
||||
|
||||
@ -66,7 +65,7 @@ public class ChangeNumberManager {
|
||||
final PhoneVerificationTokenManager phoneVerificationTokenManager,
|
||||
final RegistrationLockVerificationManager registrationLockVerificationManager,
|
||||
final RateLimiters rateLimiters,
|
||||
final Duration postRegistrationWaitingPeriod,
|
||||
final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager,
|
||||
final Clock clock) {
|
||||
|
||||
this.messageSender = messageSender;
|
||||
@ -74,7 +73,7 @@ public class ChangeNumberManager {
|
||||
this.phoneVerificationTokenManager = phoneVerificationTokenManager;
|
||||
this.registrationLockVerificationManager = registrationLockVerificationManager;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.postRegistrationWaitingPeriod = postRegistrationWaitingPeriod;
|
||||
this.changeNumberWaitingPeriodManager = changeNumberWaitingPeriodManager;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@ -101,11 +100,10 @@ public class ChangeNumberManager {
|
||||
// Only verify and check reglock if there's a data change to be made...
|
||||
if (!account.getNumber().equals(number)) {
|
||||
|
||||
final Instant registration = Instant.ofEpochMilli(account.getPrimaryDevice().getCreated());
|
||||
final Duration waitingPeriodRemaining = Duration.between(clock.instant().minus(postRegistrationWaitingPeriod), registration);
|
||||
if (waitingPeriodRemaining.isPositive()) {
|
||||
final Optional<Duration> waitingPeriodRemaining = changeNumberWaitingPeriodManager.getWaitingPeriodRemaining(account.getUuid());
|
||||
if (waitingPeriodRemaining.isPresent()) {
|
||||
Metrics.counter(POST_REGISTRATION_WAITING_PERIOD_NOT_MET_COUNTER_NAME).increment();
|
||||
throw new RateLimitExceededException(waitingPeriodRemaining);
|
||||
throw new RateLimitExceededException(waitingPeriodRemaining.get());
|
||||
}
|
||||
|
||||
rateLimiters.getRegistrationLimiter().validate(number);
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.lettuce.core.SetArgs;
|
||||
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 Duration waitingPeriod;
|
||||
|
||||
public ChangeNumberWaitingPeriodManager(final FaultTolerantRedisClusterClient redisCluster, final Duration waitingPeriod) {
|
||||
this.redisCluster = redisCluster;
|
||||
this.waitingPeriod = waitingPeriod;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// 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 + "}";
|
||||
}
|
||||
}
|
||||
@ -52,6 +52,7 @@ import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecoveryC
|
||||
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.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamoDbRecoveryManager;
|
||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||
@ -281,9 +282,11 @@ public record CommandDependencies(
|
||||
configuration.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||
RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
|
||||
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
|
||||
final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager = new ChangeNumberWaitingPeriodManager(
|
||||
rateLimitersCluster, configuration.getChangeNumber().postRegistrationWaitingPeriod());
|
||||
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||
pubsubClient, accountLockManager, keys, messagesManager, profilesManager,
|
||||
secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager,
|
||||
changeNumberWaitingPeriodManager, secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager,
|
||||
registrationRecoveryPasswordsManager, accountLockExecutor, messagePollExecutor,
|
||||
retryExecutor, clock, configuration.getLinkDeviceSecretConfiguration().secret().value());
|
||||
RateLimiters rateLimiters = RateLimiters.create(dynamicConfigurationManager, rateLimitersCluster, retryExecutor);
|
||||
|
||||
@ -140,6 +140,10 @@ public class AccountCreationDeletionIntegrationTest {
|
||||
disconnectionRequestManager = mock(DisconnectionRequestManager.class);
|
||||
when(disconnectionRequestManager.requestDisconnection(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager = mock(ChangeNumberWaitingPeriodManager.class);
|
||||
when(changeNumberWaitingPeriodManager.handleAccountCreated(any(UUID.class), any(Instant.class)))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
accountsManager = new AccountsManager(
|
||||
accounts,
|
||||
phoneNumberIdentifiers,
|
||||
@ -149,6 +153,7 @@ public class AccountCreationDeletionIntegrationTest {
|
||||
keysManager,
|
||||
messagesManager,
|
||||
profilesManager,
|
||||
changeNumberWaitingPeriodManager,
|
||||
secureStorageClient,
|
||||
svr2Client,
|
||||
disconnectionRequestManager,
|
||||
|
||||
@ -138,6 +138,7 @@ class AccountsManagerChangeNumberIntegrationTest {
|
||||
keysManager,
|
||||
messagesManager,
|
||||
profilesManager,
|
||||
mock(ChangeNumberWaitingPeriodManager.class),
|
||||
secureStorageClient,
|
||||
svr2Client,
|
||||
disconnectionRequestManager,
|
||||
|
||||
@ -119,6 +119,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
|
||||
mock(KeysManager.class),
|
||||
mock(MessagesManager.class),
|
||||
mock(ProfilesManager.class),
|
||||
mock(ChangeNumberWaitingPeriodManager.class),
|
||||
mock(SecureStorageClient.class),
|
||||
mock(SecureValueRecoveryClient.class),
|
||||
mock(DisconnectionRequestManager.class),
|
||||
|
||||
@ -64,6 +64,7 @@ public class AccountsManagerDeviceTransferIntegrationTest {
|
||||
mock(KeysManager.class),
|
||||
mock(MessagesManager.class),
|
||||
mock(ProfilesManager.class),
|
||||
mock(ChangeNumberWaitingPeriodManager.class),
|
||||
mock(SecureStorageClient.class),
|
||||
mock(SecureValueRecoveryClient.class),
|
||||
mock(DisconnectionRequestManager.class),
|
||||
|
||||
@ -147,6 +147,7 @@ class AccountsManagerTest {
|
||||
messagesManager = mock(MessagesManager.class);
|
||||
profilesManager = mock(ProfilesManager.class);
|
||||
disconnectionRequestManager = mock(DisconnectionRequestManager.class);
|
||||
final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager = mock(ChangeNumberWaitingPeriodManager.class);
|
||||
|
||||
//noinspection unchecked
|
||||
asyncCommands = mock(RedisAsyncCommands.class);
|
||||
@ -183,7 +184,7 @@ class AccountsManagerTest {
|
||||
|
||||
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString())).thenAnswer((Answer<CompletableFuture<UUID>>) invocation -> {
|
||||
final String number = invocation.getArgument(0, String.class);
|
||||
return CompletableFuture.completedFuture(phoneNumberIdentifiersByE164.computeIfAbsent(number, n -> UUID.randomUUID()));
|
||||
return CompletableFuture.completedFuture(phoneNumberIdentifiersByE164.computeIfAbsent(number, _ -> UUID.randomUUID()));
|
||||
});
|
||||
|
||||
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
|
||||
@ -214,6 +215,8 @@ class AccountsManagerTest {
|
||||
.build();
|
||||
|
||||
when(disconnectionRequestManager.requestDisconnection(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(changeNumberWaitingPeriodManager.handleAccountCreated(any(UUID.class), any(Instant.class)))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
accountsManager = new AccountsManager(
|
||||
accounts,
|
||||
@ -224,6 +227,7 @@ class AccountsManagerTest {
|
||||
keysManager,
|
||||
messagesManager,
|
||||
profilesManager,
|
||||
changeNumberWaitingPeriodManager,
|
||||
storageClient,
|
||||
svr2Client,
|
||||
disconnectionRequestManager,
|
||||
|
||||
@ -134,6 +134,10 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
final DisconnectionRequestManager disconnectionRequestManager = mock(DisconnectionRequestManager.class);
|
||||
when(disconnectionRequestManager.requestDisconnection(any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager = mock(ChangeNumberWaitingPeriodManager.class);
|
||||
when(changeNumberWaitingPeriodManager.handleAccountCreated(any(UUID.class), any(Instant.class)))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
accountsManager = new AccountsManager(
|
||||
accounts,
|
||||
phoneNumberIdentifiers,
|
||||
@ -143,6 +147,7 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
keysManager,
|
||||
messageManager,
|
||||
profileManager,
|
||||
changeNumberWaitingPeriodManager,
|
||||
mock(SecureStorageClient.class),
|
||||
mock(SecureValueRecoveryClient.class),
|
||||
disconnectionRequestManager,
|
||||
@ -150,7 +155,7 @@ class AccountsManagerUsernameIntegrationTest {
|
||||
Executors.newSingleThreadExecutor(),
|
||||
Executors.newSingleThreadScheduledExecutor(),
|
||||
Executors.newSingleThreadScheduledExecutor(),
|
||||
mock(Clock.class),
|
||||
Clock.systemUTC(),
|
||||
"link-device-secret".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
|
||||
@ -148,6 +148,7 @@ public class AddRemoveDeviceIntegrationTest {
|
||||
keysManager,
|
||||
messagesManager,
|
||||
profilesManager,
|
||||
mock(ChangeNumberWaitingPeriodManager.class),
|
||||
secureStorageClient,
|
||||
svr2Client,
|
||||
mock(DisconnectionRequestManager.class),
|
||||
|
||||
@ -13,6 +13,7 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
@ -23,7 +24,6 @@ import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@ -66,6 +66,7 @@ public class ChangeNumberManagerTest {
|
||||
private AccountsManager accountsManager;
|
||||
private PhoneVerificationTokenManager phoneVerificationTokenManager;
|
||||
private RegistrationLockVerificationManager registrationLockVerificationManager;
|
||||
private ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager;
|
||||
private RateLimiter rateLimiter;
|
||||
|
||||
private ChangeNumberManager changeNumberManager;
|
||||
@ -74,21 +75,14 @@ public class ChangeNumberManagerTest {
|
||||
|
||||
private static final TestClock CLOCK = TestClock.pinned(Instant.now());
|
||||
|
||||
private static final Duration POST_REGISTRATION_WAITING_PERIOD = Duration.ofHours(2);
|
||||
private static final Device DEFAULT_PRIMARY_DEVICE;
|
||||
static {
|
||||
DEFAULT_PRIMARY_DEVICE = new Device();
|
||||
DEFAULT_PRIMARY_DEVICE.setId((byte) 1);
|
||||
DEFAULT_PRIMARY_DEVICE.setCreated(CLOCK.instant().minus(POST_REGISTRATION_WAITING_PERIOD).minusSeconds(1).toEpochMilli());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
messageSender = mock(MessageSender.class);
|
||||
accountsManager = mock(AccountsManager.class);
|
||||
registrationLockVerificationManager = mock(RegistrationLockVerificationManager.class);
|
||||
rateLimiter = mock(RateLimiter.class);
|
||||
phoneVerificationTokenManager = mock(PhoneVerificationTokenManager.class);
|
||||
changeNumberWaitingPeriodManager = mock(ChangeNumberWaitingPeriodManager.class);
|
||||
rateLimiter = mock(RateLimiter.class);
|
||||
|
||||
when(phoneVerificationTokenManager.verify(any(), any(), any(), any())).thenAnswer(invocation -> {
|
||||
final byte[] sessionId = invocation.getArgument(2);
|
||||
@ -98,12 +92,14 @@ public class ChangeNumberManagerTest {
|
||||
: PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD;
|
||||
});
|
||||
|
||||
when(changeNumberWaitingPeriodManager.getWaitingPeriodRemaining(any())).thenReturn(Optional.empty());
|
||||
|
||||
final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
when(rateLimiters.getRegistrationLimiter()).thenReturn(rateLimiter);
|
||||
|
||||
changeNumberManager = new ChangeNumberManager(messageSender, accountsManager,
|
||||
phoneVerificationTokenManager, registrationLockVerificationManager, rateLimiters,
|
||||
POST_REGISTRATION_WAITING_PERIOD, CLOCK);
|
||||
changeNumberWaitingPeriodManager, CLOCK);
|
||||
|
||||
updatedPhoneNumberIdentifiersByAccount = new HashMap<>();
|
||||
|
||||
@ -163,7 +159,6 @@ public class ChangeNumberManagerTest {
|
||||
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
|
||||
when(account.isIdentifiedBy(any())).thenReturn(false);
|
||||
when(account.isIdentifiedBy(new AciServiceIdentifier(accountIdentifier))).thenReturn(true);
|
||||
when(account.getPrimaryDevice()).thenReturn(DEFAULT_PRIMARY_DEVICE);
|
||||
|
||||
when(accountsManager.getAccountsForChangeNumber(eq(accountIdentifier), any()))
|
||||
.thenReturn(new Pair<>(account, Optional.empty()));
|
||||
@ -206,7 +201,6 @@ public class ChangeNumberManagerTest {
|
||||
when(account.getDevice(primaryDeviceId)).thenReturn(Optional.of(primaryDevice));
|
||||
when(account.getDevice(linkedDeviceId)).thenReturn(Optional.of(linkedDevice));
|
||||
when(account.getDevices()).thenReturn(List.of(primaryDevice, linkedDevice));
|
||||
when(account.getPrimaryDevice()).thenReturn(DEFAULT_PRIMARY_DEVICE);
|
||||
|
||||
final ECKeyPair pniIdentityKeyPair = ECKeyPair.generate();
|
||||
final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
||||
@ -289,7 +283,6 @@ public class ChangeNumberManagerTest {
|
||||
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
|
||||
when(account.isIdentifiedBy(any())).thenReturn(false);
|
||||
when(account.isIdentifiedBy(new AciServiceIdentifier(accountIdentifier))).thenReturn(true);
|
||||
when(account.getPrimaryDevice()).thenReturn(DEFAULT_PRIMARY_DEVICE);
|
||||
|
||||
when(accountsManager.getAccountsForChangeNumber(eq(accountIdentifier), any()))
|
||||
.thenReturn(new Pair<>(account, Optional.empty()));
|
||||
@ -329,7 +322,6 @@ public class ChangeNumberManagerTest {
|
||||
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
|
||||
when(account.isIdentifiedBy(any())).thenReturn(false);
|
||||
when(account.isIdentifiedBy(new AciServiceIdentifier(accountIdentifier))).thenReturn(true);
|
||||
when(account.getPrimaryDevice()).thenReturn(DEFAULT_PRIMARY_DEVICE);
|
||||
|
||||
when(accountsManager.getAccountsForChangeNumber(eq(accountIdentifier), any()))
|
||||
.thenReturn(new Pair<>(account, Optional.empty()));
|
||||
@ -378,7 +370,6 @@ public class ChangeNumberManagerTest {
|
||||
when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier);
|
||||
when(account.isIdentifiedBy(any())).thenReturn(false);
|
||||
when(account.isIdentifiedBy(new AciServiceIdentifier(accountIdentifier))).thenReturn(true);
|
||||
when(account.getPrimaryDevice()).thenReturn(DEFAULT_PRIMARY_DEVICE);
|
||||
|
||||
final Account existingAccount = mock(Account.class);
|
||||
when(existingAccount.getNumber()).thenReturn(targetNumber);
|
||||
@ -407,7 +398,13 @@ public class ChangeNumberManagerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void testRecentRegistration(final boolean expectRateLimited, final boolean sameNumber, final Instant registrationInstant) throws Throwable {
|
||||
void testRecentRegistration(final boolean expectRateLimited, final boolean sameNumber, final boolean waitingPeriodMet) throws Throwable {
|
||||
|
||||
final Duration waitingPeriod = Duration.ofMinutes(30);
|
||||
|
||||
reset(changeNumberWaitingPeriodManager);
|
||||
when(changeNumberWaitingPeriodManager.getWaitingPeriodRemaining(any()))
|
||||
.thenReturn(waitingPeriodMet ? Optional.empty() : Optional.of(waitingPeriod));
|
||||
|
||||
final String originalNumber = PhoneNumberUtil.getInstance().format(
|
||||
PhoneNumberUtil.getInstance().getExampleNumber("DE"), PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||
@ -434,10 +431,6 @@ public class ChangeNumberManagerTest {
|
||||
when(account.isIdentifiedBy(any())).thenReturn(false);
|
||||
when(account.isIdentifiedBy(new AciServiceIdentifier(accountIdentifier))).thenReturn(true);
|
||||
|
||||
final Device primaryDevice = mock(Device.class);
|
||||
when(account.getPrimaryDevice()).thenReturn(primaryDevice);
|
||||
when(primaryDevice.getCreated()).thenReturn(registrationInstant.toEpochMilli());
|
||||
|
||||
when(accountsManager.getAccountsForChangeNumber(eq(accountIdentifier), any()))
|
||||
.thenReturn(new Pair<>(account, Optional.empty()));
|
||||
|
||||
@ -454,8 +447,7 @@ public class ChangeNumberManagerTest {
|
||||
mock(ContainerRequestContext.class));
|
||||
if (expectRateLimited) {
|
||||
final RateLimitExceededException e = assertThrows(RateLimitExceededException.class, changeNumberOperation);
|
||||
|
||||
assertEquals(Duration.between(CLOCK.instant().minus(POST_REGISTRATION_WAITING_PERIOD), registrationInstant), e.getRetryDuration().orElseThrow());
|
||||
assertEquals(waitingPeriod, e.getRetryDuration().orElseThrow());
|
||||
} else {
|
||||
changeNumberOperation.execute();
|
||||
verify(accountsManager).changeNumber(accountIdentifier, targetNumber, pniIdentityKey, ecSignedPreKeys, kemLastResortPreKeys, Collections.emptyMap());
|
||||
@ -463,16 +455,11 @@ public class ChangeNumberManagerTest {
|
||||
}
|
||||
|
||||
static Collection<Arguments> testRecentRegistration() {
|
||||
// truncate to millis because that is the resolution for device.created
|
||||
final Instant tooRecent = CLOCK.instant().minus(POST_REGISTRATION_WAITING_PERIOD).plusSeconds(1)
|
||||
.truncatedTo(ChronoUnit.MILLIS);
|
||||
final Instant outsideWaitingPeriod = CLOCK.instant().minus(POST_REGISTRATION_WAITING_PERIOD).minusSeconds(1)
|
||||
.truncatedTo(ChronoUnit.MILLIS);
|
||||
return List.of(
|
||||
// expect exception, same number, registration instant
|
||||
Arguments.argumentSet("waiting period elapsed", false, false, outsideWaitingPeriod),
|
||||
Arguments.argumentSet("waiting period not elapsed", true, false, tooRecent),
|
||||
Arguments.argumentSet("waiting period not elapsed; same number", false, true, tooRecent)
|
||||
Arguments.argumentSet("waiting period elapsed", false, false, true),
|
||||
Arguments.argumentSet("waiting period not elapsed", true, false, false),
|
||||
Arguments.argumentSet("waiting period not elapsed; same number", false, true, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
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;
|
||||
|
||||
class ChangeNumberWaitingPeriodManagerTest {
|
||||
|
||||
@RegisterExtension
|
||||
static final RedisClusterExtension REDIS_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
|
||||
|
||||
private static final Duration WAITING_PERIOD = Duration.ofDays(7);
|
||||
|
||||
private ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
changeNumberWaitingPeriodManager = new ChangeNumberWaitingPeriodManager(
|
||||
REDIS_CLUSTER_EXTENSION.getRedisCluster(), WAITING_PERIOD);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNewAccount() throws Exception {
|
||||
final UUID aci = UUID.randomUUID();
|
||||
|
||||
assertTrue(changeNumberWaitingPeriodManager.getWaitingPeriodRemaining(aci).isEmpty());
|
||||
|
||||
changeNumberWaitingPeriodManager.handleAccountCreated(aci, Instant.now())
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
|
||||
assertTrue(changeNumberWaitingPeriodManager.getWaitingPeriodRemaining(aci).isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOldAccount() throws Exception {
|
||||
final UUID aci = UUID.randomUUID();
|
||||
|
||||
changeNumberWaitingPeriodManager.handleAccountCreated(aci,
|
||||
Instant.now().minus(WAITING_PERIOD).minus(Duration.ofHours(1)))
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user