diff --git a/integration-tests/src/main/java/org/signal/integration/IntegrationTools.java b/integration-tests/src/main/java/org/signal/integration/IntegrationTools.java index c7409eb64..d8f09d707 100644 --- a/integration-tests/src/main/java/org/signal/integration/IntegrationTools.java +++ b/integration-tests/src/main/java/org/signal/integration/IntegrationTools.java @@ -5,14 +5,20 @@ 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.UUID; 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.PhoneNumberIdentifiers; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; @@ -31,6 +37,7 @@ public class IntegrationTools { private final PhoneNumberIdentifiers phoneNumberIdentifiers; + private final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager; public static IntegrationTools create(final Config config) { final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.builder().build(); @@ -44,20 +51,28 @@ public class IntegrationTools { 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 PhoneNumberIdentifiers(dynamoDbAsyncClient, config.dynamoDbTables().phoneNumberIdentifiers()), + new ChangeNumberWaitingPeriodManager(rateLimitersClient, Duration.ZERO) ); } private IntegrationTools( final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, final VerificationSessionManager verificationSessionManager, - final PhoneNumberIdentifiers phoneNumberIdentifiers) { + final PhoneNumberIdentifiers phoneNumberIdentifiers, + final ChangeNumberWaitingPeriodManager changeNumberWaitingPeriodManager) { this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager; this.verificationSessionManager = verificationSessionManager; this.phoneNumberIdentifiers = phoneNumberIdentifiers; + this.changeNumberWaitingPeriodManager = changeNumberWaitingPeriodManager; } public CompletableFuture populateRecoveryPassword(final String phoneNumber, final byte[] password) { @@ -71,4 +86,13 @@ public class IntegrationTools { return verificationSessionManager.findForId(sessionId) .thenApply(maybeSession -> maybeSession.map(VerificationSession::pushChallenge)); } + + 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); + } + } } diff --git a/integration-tests/src/main/java/org/signal/integration/Operations.java b/integration-tests/src/main/java/org/signal/integration/Operations.java index fcb209a8e..d491a878b 100644 --- a/integration-tests/src/main/java/org/signal/integration/Operations.java +++ b/integration-tests/src/main/java/org/signal/integration/Operations.java @@ -128,6 +128,10 @@ public final class Operations { return recoveryPassword; } + public static void clearChangeNumberWaitingPeriod(final TestUser user) { + INTEGRATION_TOOLS.clearChangeNumberWaitingPeriod(user); + } + public static T sendEmptyRequestAuthenticated( final String endpoint, final String method, diff --git a/integration-tests/src/main/java/org/signal/integration/config/Config.java b/integration-tests/src/main/java/org/signal/integration/config/Config.java index f9aa3cbe6..36423341b 100644 --- a/integration-tests/src/main/java/org/signal/integration/config/Config.java +++ b/integration-tests/src/main/java/org/signal/integration/config/Config.java @@ -15,5 +15,6 @@ public record Config(@NotBlank String domain, @NotNull @Valid DynamoDbClientFactory dynamoDbClient, @NotNull @Valid DynamoDbTables dynamoDbTables, @NotBlank String prescribedRegistrationNumber, - @NotBlank String prescribedRegistrationCode) { + @NotBlank String prescribedRegistrationCode, + @NotNull @Valid Redis redis) { } diff --git a/integration-tests/src/main/java/org/signal/integration/config/Redis.java b/integration-tests/src/main/java/org/signal/integration/config/Redis.java new file mode 100644 index 000000000..aae4f1584 --- /dev/null +++ b/integration-tests/src/main/java/org/signal/integration/config/Redis.java @@ -0,0 +1,13 @@ +/* + * 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) { +} diff --git a/integration-tests/src/test/java/org/signal/integration/AccountTest.java b/integration-tests/src/test/java/org/signal/integration/AccountTest.java index 1edde19c1..a570721ab 100644 --- a/integration-tests/src/test/java/org/signal/integration/AccountTest.java +++ b/integration-tests/src/test/java/org/signal/integration/AccountTest.java @@ -62,14 +62,20 @@ public class AccountTest { Map.of(Device.PRIMARY_ID, Operations.generateSignedKEMPreKey(2, pniIdentityKeyPair)), Map.of(Device.PRIMARY_ID, 17)); - final AccountIdentityResponse accountIdentityResponse = - Operations.apiPut("/v2/accounts/number", changeNumberRequest) - .authorized(user) - .executeExpectSuccess(AccountIdentityResponse.class); + try { + Operations.clearChangeNumberWaitingPeriod(user); - assertEquals(user.aciUuid(), accountIdentityResponse.uuid()); - assertNotEquals(user.pniUuid(), accountIdentityResponse.pni()); - assertEquals(targetNumber, accountIdentityResponse.number()); + final AccountIdentityResponse accountIdentityResponse = + Operations.apiPut("/v2/accounts/number", changeNumberRequest) + .authorized(user) + .executeExpectSuccess(AccountIdentityResponse.class); + + assertEquals(user.aciUuid(), accountIdentityResponse.uuid()); + assertNotEquals(user.pniUuid(), accountIdentityResponse.pni()); + assertEquals(targetNumber, accountIdentityResponse.number()); + } finally { + Operations.deleteUser(user); + } } @Test