Make username-related operations blocking

This commit is contained in:
Jon Chambers
2026-02-26 17:20:34 -05:00
committed by GitHub
parent ad9c03186a
commit 5d306f8d15
10 changed files with 611 additions and 635 deletions

View File

@@ -504,9 +504,9 @@ class AccountControllerTest {
}
@Test
void testReserveUsernameHash() {
void testReserveUsernameHash() throws UsernameHashNotAvailableException {
when(accountsManager.reserveUsernameHash(any(), any()))
.thenReturn(CompletableFuture.completedFuture(new AccountsManager.UsernameReservation(null, USERNAME_HASH_1)));
.thenReturn(new AccountsManager.UsernameReservation(null, USERNAME_HASH_1));
try (final Response response = resources.getJerseyTest()
.target("/v1/accounts/username_hash/reserve")
@@ -521,9 +521,9 @@ class AccountControllerTest {
}
@Test
void testReserveUsernameHashUnavailable() {
void testReserveUsernameHashUnavailable() throws UsernameHashNotAvailableException {
when(accountsManager.reserveUsernameHash(any(), anyList()))
.thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException()));
.thenThrow(new UsernameHashNotAvailableException());
try (final Response response = resources.getJerseyTest()
.target("/v1/accounts/username_hash/reserve")
@@ -604,13 +604,14 @@ class AccountControllerTest {
}
@Test
void testConfirmUsernameHash() throws BaseUsernameException {
void testConfirmUsernameHash()
throws BaseUsernameException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
Account account = mock(Account.class);
final UUID uuid = UUID.randomUUID();
when(account.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH_1));
when(account.getUsernameLinkHandle()).thenReturn(uuid);
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1)))
.thenReturn(CompletableFuture.completedFuture(account));
.thenReturn(account);
try (final Response response = resources.getJerseyTest()
.target("/v1/accounts/username_hash/confirm")
@@ -641,12 +642,13 @@ class AccountControllerTest {
}
@Test
void testConfirmUsernameHashOld() throws BaseUsernameException {
void testConfirmUsernameHashOld()
throws BaseUsernameException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
Account account = mock(Account.class);
when(account.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH_1));
when(account.getUsernameLinkHandle()).thenReturn(null);
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1), eq(null)))
.thenReturn(CompletableFuture.completedFuture(account));
.thenReturn(account);
try (final Response response = resources.getJerseyTest()
.target("/v1/accounts/username_hash/confirm")
@@ -664,9 +666,10 @@ class AccountControllerTest {
}
@Test
void testConfirmUnreservedUsernameHash() throws BaseUsernameException {
void testConfirmUnreservedUsernameHash()
throws BaseUsernameException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1), any()))
.thenReturn(CompletableFuture.failedFuture(new UsernameReservationNotFoundException()));
.thenThrow(new UsernameReservationNotFoundException());
try (final Response response = resources.getJerseyTest()
.target("/v1/accounts/username_hash/confirm")
@@ -680,9 +683,10 @@ class AccountControllerTest {
}
@Test
void testConfirmLapsedUsernameHash() throws BaseUsernameException {
void testConfirmLapsedUsernameHash()
throws BaseUsernameException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1), any()))
.thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException()));
.thenThrow(new UsernameHashNotAvailableException());
try (final Response response = resources.getJerseyTest()
.target("/v1/accounts/username_hash/confirm")
@@ -748,7 +752,7 @@ class AccountControllerTest {
@Test
void testDeleteUsername() {
when(accountsManager.clearUsernameHash(any()))
.thenAnswer(invocation -> CompletableFutureTestUtil.almostCompletedFuture(invocation.getArgument(0)));
.thenAnswer(invocation -> invocation.getArgument(0));
try (final Response response = resources.getJerseyTest()
.target("/v1/accounts/username_hash/")
@@ -756,7 +760,6 @@ class AccountControllerTest {
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.delete()) {
assertThat(response.readEntity(String.class)).isEqualTo("");
assertThat(response.getStatus()).isEqualTo(204);
verify(accountsManager).clearUsernameHash(AuthHelper.VALID_ACCOUNT);
}

View File

@@ -245,7 +245,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
}
@Test
void reserveUsernameHash() {
void reserveUsernameHash() throws UsernameHashNotAvailableException {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifier(AUTHENTICATED_ACI))
@@ -257,8 +257,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
.thenAnswer(invocation -> {
final List<byte[]> usernameHashes = invocation.getArgument(1);
return CompletableFuture.completedFuture(
new AccountsManager.UsernameReservation(invocation.getArgument(0), usernameHashes.getFirst()));
return new AccountsManager.UsernameReservation(invocation.getArgument(0), usernameHashes.getFirst());
});
final ReserveUsernameHashResponse expectedResponse = ReserveUsernameHashResponse.newBuilder()
@@ -272,7 +271,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
}
@Test
void reserveUsernameHashNotAvailable() {
void reserveUsernameHashNotAvailable() throws UsernameHashNotAvailableException {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifier(AUTHENTICATED_ACI))
@@ -281,7 +280,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
final byte[] usernameHash = TestRandomUtil.nextBytes(AccountController.USERNAME_HASH_LENGTH);
when(accountsManager.reserveUsernameHash(any(), any()))
.thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException()));
.thenThrow(new UsernameHashNotAvailableException());
final ReserveUsernameHashResponse expectedResponse = ReserveUsernameHashResponse.newBuilder()
.setUsernameNotAvailable(UsernameNotAvailable.getDefaultInstance())
@@ -348,7 +347,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
}
@Test
void confirmUsernameHash() {
void confirmUsernameHash() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
final byte[] usernameHash = TestRandomUtil.nextBytes(AccountController.USERNAME_HASH_LENGTH);
final byte[] usernameCiphertext = TestRandomUtil.nextBytes(32);
@@ -369,7 +368,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
when(updatedAccount.getUsernameHash()).thenReturn(Optional.of(usernameHash));
when(updatedAccount.getUsernameLinkHandle()).thenReturn(linkHandle);
return CompletableFuture.completedFuture(updatedAccount);
return updatedAccount;
});
final ConfirmUsernameHashResponse expectedResponse = ConfirmUsernameHashResponse.newBuilder()
@@ -389,7 +388,8 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
@ParameterizedTest
@MethodSource
void confirmUsernameHashConfirmationException(final Exception confirmationException, final ConfirmUsernameHashResponse expectedResponse) {
void confirmUsernameHashConfirmationException(final Exception confirmationException, final ConfirmUsernameHashResponse expectedResponse)
throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
final byte[] usernameHash = TestRandomUtil.nextBytes(AccountController.USERNAME_HASH_LENGTH);
final byte[] usernameCiphertext = TestRandomUtil.nextBytes(32);
@@ -402,7 +402,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
.thenReturn(Optional.of(account));
when(accountsManager.confirmReservedUsernameHash(any(), any(), any()))
.thenReturn(CompletableFuture.failedFuture(confirmationException));
.thenThrow(confirmationException);
final ConfirmUsernameHashResponse actualResponse = authenticatedServiceStub()
.confirmUsernameHash(ConfirmUsernameHashRequest.newBuilder()
@@ -500,7 +500,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
when(accountsManager.getByAccountIdentifier(AUTHENTICATED_ACI))
.thenReturn(Optional.of(account));
when(accountsManager.clearUsernameHash(account)).thenReturn(CompletableFuture.completedFuture(account));
when(accountsManager.clearUsernameHash(account)).thenReturn(account);
assertDoesNotThrow(() ->
authenticatedServiceStub().deleteUsernameHash(DeleteUsernameHashRequest.newBuilder().build()));

View File

@@ -97,7 +97,6 @@ import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
import org.whispersystems.textsecuregcm.tests.util.MockRedisFuture;
import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
import org.whispersystems.textsecuregcm.tests.util.RedisServerHelper;
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.TestClock;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
@@ -1214,53 +1213,56 @@ class AccountsManagerTest {
}
@Test
void testReserveUsernameHash() {
void testReserveUsernameHash() throws UsernameHashNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accounts.getByAccountIdentifier(account.getUuid())).thenReturn(Optional.of(account));
final List<byte[]> usernameHashes = List.of(TestRandomUtil.nextBytes(32), TestRandomUtil.nextBytes(32));
when(accounts.reserveUsernameHash(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes).join();
assertArrayEquals(usernameHashes.get(0), result.reservedUsernameHash());
final UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes);
assertArrayEquals(usernameHashes.getFirst(), result.reservedUsernameHash());
verify(accounts, times(1)).reserveUsernameHash(eq(account), any(), eq(Duration.ofMinutes(5)));
}
@Test
void testReserveOwnUsernameHash() {
void testReserveOwnUsernameHash() throws UsernameHashNotAvailableException {
final byte[] oldUsernameHash = TestRandomUtil.nextBytes(32);
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
account.setUsernameHash(oldUsernameHash);
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accounts.getByAccountIdentifier(account.getUuid())).thenReturn(Optional.of(account));
final List<byte[]> usernameHashes = List.of(TestRandomUtil.nextBytes(32), oldUsernameHash, TestRandomUtil.nextBytes(32));
UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes).join();
final UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes);
assertArrayEquals(oldUsernameHash, result.reservedUsernameHash());
verify(accounts, never()).reserveUsernameHash(any(), any(), any());
}
@Test
void testReserveUsernameOptimisticLockingFailure() {
void testReserveUsernameOptimisticLockingFailure() throws UsernameHashNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accounts.getByAccountIdentifier(account.getUuid())).thenReturn(Optional.of(account));
final List<byte[]> usernameHashes = List.of(TestRandomUtil.nextBytes(32), TestRandomUtil.nextBytes(32));
when(accounts.reserveUsernameHash(any(), any(), any()))
.thenReturn(CompletableFuture.failedFuture(new ContestedOptimisticLockException()))
.thenReturn(CompletableFuture.completedFuture(null));
UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes).join();
assertArrayEquals(usernameHashes.get(0), result.reservedUsernameHash());
doThrow(new ContestedOptimisticLockException())
.doNothing()
.when(accounts).reserveUsernameHash(any(), any(), any());
final UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes);
assertArrayEquals(usernameHashes.getFirst(), result.reservedUsernameHash());
verify(accounts, times(2)).reserveUsernameHash(eq(account), any(), eq(Duration.ofMinutes(5)));
}
@Test
void testReserveUsernameHashNotAvailable() {
void testReserveUsernameHashAsyncNotAvailable() throws UsernameHashNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accounts.reserveUsernameHash(any(), any(), any())).thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException()));
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
doThrow(new UsernameHashNotAvailableException())
.when(accounts).reserveUsernameHash(any(), any(), any());
assertThrows(UsernameHashNotAvailableException.class, () ->
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1, USERNAME_HASH_2)));
}
@@ -1269,10 +1271,7 @@ class AccountsManagerTest {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
setReservationHash(account, USERNAME_HASH_1);
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
.thenReturn(CompletableFuture.completedFuture(null));
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
verify(accounts).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1));
}
@@ -1280,13 +1279,13 @@ class AccountsManagerTest {
void testConfirmReservedUsernameHashOptimisticLockingFailure() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
setReservationHash(account, USERNAME_HASH_1);
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accounts.getByAccountIdentifier(account.getUuid())).thenReturn(Optional.of(account));
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
.thenReturn(CompletableFuture.failedFuture(new ContestedOptimisticLockException()))
.thenReturn(CompletableFuture.completedFuture(null));
doThrow(new ContestedOptimisticLockException())
.doNothing()
.when(accounts).confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
verify(accounts, times(2)).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1));
}
@@ -1294,21 +1293,19 @@ class AccountsManagerTest {
void testConfirmReservedHashNameMismatch() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
setReservationHash(account, USERNAME_HASH_1);
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
.thenReturn(CompletableFuture.completedFuture(null));
CompletableFutureTestUtil.assertFailsWithCause(UsernameReservationNotFoundException.class,
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2));
assertThrows(UsernameReservationNotFoundException.class,
() -> accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2));
}
@Test
void testConfirmReservedLapsed() {
void testConfirmReservedLapsed() throws UsernameHashNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
// hash was reserved, but the reservation lapsed and another account took it
setReservationHash(account, USERNAME_HASH_1);
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
.thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException()));
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
doThrow(new UsernameHashNotAvailableException())
.when(accounts).confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
assertThrows(UsernameHashNotAvailableException.class,
() -> accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertTrue(account.getUsernameHash().isEmpty());
}
@@ -1318,27 +1315,24 @@ class AccountsManagerTest {
account.setUsernameHash(USERNAME_HASH_1);
// reserved username already set, should be treated as a replay
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
verifyNoInteractions(accounts);
}
@Test
void testConfirmReservedUsernameHashWithNoReservation() {
void testConfirmReservedUsernameHashWithNoReservation() throws UsernameHashNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
CompletableFutureTestUtil.assertFailsWithCause(UsernameReservationNotFoundException.class,
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThrows(UsernameReservationNotFoundException.class,
() -> accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
verify(accounts, never()).confirmUsernameHash(any(), any(), any());
}
@Test
void testClearUsernameHash() {
when(accounts.clearUsernameHash(any()))
.thenReturn(CompletableFuture.completedFuture(null));
Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]);
account.setUsernameHash(USERNAME_HASH_1);
accountsManager.clearUsernameHash(account).join();
accountsManager.clearUsernameHash(account);
verify(accounts).clearUsernameHash(eq(account));
}

View File

@@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.storage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -46,7 +47,6 @@ import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecoveryC
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
@@ -190,8 +190,8 @@ class AccountsManagerUsernameIntegrationTest {
.build());
}
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accountsManager.reserveUsernameHash(account, usernameHashes));
assertThrows(UsernameHashNotAvailableException.class,
() -> accountsManager.reserveUsernameHash(account, usernameHashes));
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsernameHash()).isEmpty();
}
@@ -217,19 +217,19 @@ class AccountsManagerUsernameIntegrationTest {
final byte[] username = accountsManager
.reserveUsernameHash(account, usernameHashes)
.join()
.reservedUsernameHash();
assertArrayEquals(username, availableHash);
}
@Test
public void testReserveConfirmClear() throws InterruptedException {
public void testReserveConfirmClear()
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
Account account = AccountsHelper.createAccount(accountsManager, "+18005551111");
// reserve
AccountsManager.UsernameReservation reservation =
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1)).join();
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1));
assertArrayEquals(reservation.account().getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
assertThat(accountsManager.getByUsernameHash(reservation.reservedUsernameHash()).join()).isEmpty();
@@ -238,7 +238,7 @@ class AccountsManagerUsernameIntegrationTest {
account = accountsManager.confirmReservedUsernameHash(
reservation.account(),
reservation.reservedUsernameHash(),
ENCRYPTED_USERNAME_1).join();
ENCRYPTED_USERNAME_1);
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1).join().orElseThrow().getUuid()).isEqualTo(
account.getUuid());
@@ -247,43 +247,45 @@ class AccountsManagerUsernameIntegrationTest {
.isEqualTo(account.getUuid());
// clear
account = accountsManager.clearUsernameHash(account).join();
account = accountsManager.clearUsernameHash(account);
assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsernameHash()).isEmpty();
}
@Test
public void testHold() throws InterruptedException {
public void testHold()
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
Account account = AccountsHelper.createAccount(accountsManager, "+18005551111");
AccountsManager.UsernameReservation reservation =
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1)).join();
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1));
// confirm
account = accountsManager.confirmReservedUsernameHash(
reservation.account(),
reservation.reservedUsernameHash(),
ENCRYPTED_USERNAME_1).join();
ENCRYPTED_USERNAME_1);
// clear
account = accountsManager.clearUsernameHash(account).join();
account = accountsManager.clearUsernameHash(account);
assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsernameHash()).isEmpty();
assertThat(accountsManager.getByUsernameHash(reservation.reservedUsernameHash()).join()).isEmpty();
Account account2 = AccountsHelper.createAccount(accountsManager, "+18005552222");
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accountsManager.reserveUsernameHash(account2, List.of(USERNAME_HASH_1)),
assertThrows(UsernameHashNotAvailableException.class,
() -> accountsManager.reserveUsernameHash(account2, List.of(USERNAME_HASH_1)),
"account2 should not be able to reserve a held hash");
}
@Test
public void testReservationLapsed() throws InterruptedException {
public void testReservationLapsed()
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
final Account account = AccountsHelper.createAccount(accountsManager, "+18005551111");
AccountsManager.UsernameReservation reservation1 =
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1)).join();
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1));
long past = Instant.now().minus(Duration.ofMinutes(1)).getEpochSecond();
// force expiration
@@ -299,29 +301,30 @@ class AccountsManagerUsernameIntegrationTest {
Account account2 = AccountsHelper.createAccount(accountsManager, "+18005552222");
final AccountsManager.UsernameReservation reservation2 =
accountsManager.reserveUsernameHash(account2, List.of(USERNAME_HASH_1)).join();
accountsManager.reserveUsernameHash(account2, List.of(USERNAME_HASH_1));
assertArrayEquals(reservation2.reservedUsernameHash(), USERNAME_HASH_1);
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
account2 = accountsManager.confirmReservedUsernameHash(reservation2.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
assertThrows(UsernameHashNotAvailableException.class,
() -> accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
account2 = accountsManager.confirmReservedUsernameHash(reservation2.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
assertEquals(accountsManager.getByUsernameHash(USERNAME_HASH_1).join().orElseThrow().getUuid(), account2.getUuid());
assertArrayEquals(account2.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
}
@Test
void testUsernameSetReserveAnotherClearSetReserved() throws InterruptedException {
void testUsernameSetReserveAnotherClearSetReserved()
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
Account account = AccountsHelper.createAccount(accountsManager, "+18005551111");
// Set username hash
final AccountsManager.UsernameReservation reservation1 =
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1)).join();
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1));
account = accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
account = accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
// Reserve another hash on the same account
final AccountsManager.UsernameReservation reservation2 =
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_2)).join();
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_2));
account = reservation2.account();
@@ -330,23 +333,23 @@ class AccountsManagerUsernameIntegrationTest {
assertArrayEquals(account.getEncryptedUsername().orElseThrow(), ENCRYPTED_USERNAME_1);
// Clear the set username hash but not the reserved one
account = accountsManager.clearUsernameHash(account).join();
account = accountsManager.clearUsernameHash(account);
assertThat(account.getReservedUsernameHash()).isPresent();
assertThat(account.getUsernameHash()).isEmpty();
// Confirm second reservation
account = accountsManager.confirmReservedUsernameHash(account, reservation2.reservedUsernameHash(), ENCRYPTED_USERNAME_2).join();
account = accountsManager.confirmReservedUsernameHash(account, reservation2.reservedUsernameHash(), ENCRYPTED_USERNAME_2);
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_2);
assertArrayEquals(account.getEncryptedUsername().orElseThrow(), ENCRYPTED_USERNAME_2);
}
@Test
public void testReclaim() throws InterruptedException {
public void testReclaim()
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
Account account = AccountsHelper.createAccount(accountsManager, "+18005551111");
final AccountsManager.UsernameReservation reservation1 =
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1)).join();
account = accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1)
.join();
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1));
account = accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
// "reclaim" the account by re-registering
Account reclaimed = AccountsHelper.createAccount(accountsManager, "+18005551111");
@@ -358,7 +361,7 @@ class AccountsManagerUsernameIntegrationTest {
assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
// confirm it again
accountsManager.confirmReservedUsernameHash(reclaimed, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accountsManager.confirmReservedUsernameHash(reclaimed, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1).join()).isPresent();
}

View File

@@ -65,7 +65,6 @@ import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema.Tables;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.tests.util.DevicesHelper;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.TestClock;
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
@@ -394,7 +393,7 @@ class AccountsTest {
@ParameterizedTest
@EnumSource(UsernameStatus.class)
void reclaimAccountWithNoUsername(UsernameStatus usernameStatus) {
void reclaimAccountWithNoUsername(UsernameStatus usernameStatus) throws UsernameHashNotAvailableException {
Device device = generateDevice(DEVICE_ID_1);
UUID firstUuid = UUID.randomUUID();
UUID firstPni = UUID.randomUUID();
@@ -407,12 +406,12 @@ class AccountsTest {
case NONE:
break;
case RESERVED:
accounts.reserveUsernameHash(account, TestRandomUtil.nextBytes(32), Duration.ofMinutes(1)).join();
accounts.reserveUsernameHash(account, TestRandomUtil.nextBytes(32), Duration.ofMinutes(1));
break;
case RESERVED_WITH_SAVED_LINK:
// give the account a username
accounts.reserveUsernameHash(account, usernameHash, Duration.ofMinutes(1)).join();
accounts.confirmUsernameHash(account, usernameHash, encryptedUsername).join();
accounts.reserveUsernameHash(account, usernameHash, Duration.ofMinutes(1));
accounts.confirmUsernameHash(account, usernameHash, encryptedUsername);
// simulate a partially-completed re-reg: we give the account a reclaimable username, but we'll try
// re-registering again later in the test case
@@ -420,8 +419,8 @@ class AccountsTest {
reclaimAccount(account);
break;
case CONFIRMED:
accounts.reserveUsernameHash(account, usernameHash, Duration.ofMinutes(1)).join();
accounts.confirmUsernameHash(account, usernameHash, encryptedUsername).join();
accounts.reserveUsernameHash(account, usernameHash, Duration.ofMinutes(1));
accounts.confirmUsernameHash(account, usernameHash, encryptedUsername);
break;
}
@@ -433,7 +432,7 @@ class AccountsTest {
// If we had a username link, or we had previously saved a username link from another re-registration, make sure
// we preserve it
accounts.confirmUsernameHash(account, usernameHash, encryptedUsername).join();
accounts.confirmUsernameHash(account, usernameHash, encryptedUsername);
boolean shouldReuseLink = switch (usernameStatus) {
case RESERVED_WITH_SAVED_LINK, CONFIRMED -> true;
@@ -488,7 +487,7 @@ class AccountsTest {
}
@Test
void testReclaimAccount() {
void testReclaimAccount() throws UsernameHashNotAvailableException {
final String e164 = "+14151112222";
final Device device = generateDevice(DEVICE_ID_1);
final UUID existingUuid = UUID.randomUUID();
@@ -505,7 +504,7 @@ class AccountsTest {
final byte[] encryptedUsername = TestRandomUtil.nextBytes(16);
// Set up the existing account to have a username hash
accounts.confirmUsernameHash(existingAccount, usernameHash, encryptedUsername).join();
accounts.confirmUsernameHash(existingAccount, usernameHash, encryptedUsername);
final UUID usernameLinkHandle = existingAccount.getUsernameLinkHandle();
verifyStoredState(e164, existingAccount.getUuid(), existingAccount.getPhoneNumberIdentifier(), usernameHash, existingAccount, true);
@@ -538,7 +537,7 @@ class AccountsTest {
assertThat(result.getBackupVoucher()).isEqualTo(bv);
// should keep the same usernameLink, now encryptedUsername should be set
accounts.confirmUsernameHash(result, usernameHash, encryptedUsername).join();
accounts.confirmUsernameHash(result, usernameHash, encryptedUsername);
item = readAccount(existingUuid);
result = Accounts.fromItem(item);
assertThat(AttributeValues.getUUID(item, Accounts.ATTR_USERNAME_LINK_UUID, null))
@@ -1025,14 +1024,14 @@ class AccountsTest {
}
@Test
void testSwitchUsernameHashes() {
void testSwitchUsernameHashes() throws UsernameHashNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
final UUID oldHandle = account.getUsernameLinkHandle();
{
@@ -1043,8 +1042,8 @@ class AccountsTest {
verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(), USERNAME_HASH_1, maybeAccount2.orElseThrow(), account);
}
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2);
final UUID newHandle = account.getUsernameLinkHandle();
// switching usernames should put a hold on our original username
@@ -1079,8 +1078,8 @@ class AccountsTest {
// first account reserves and confirms username hash
assertThatNoException().isThrownBy(() -> {
accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
});
final Optional<Account> maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_1).join();
@@ -1089,16 +1088,16 @@ class AccountsTest {
verifyStoredState(firstAccount.getNumber(), firstAccount.getUuid(), firstAccount.getPhoneNumberIdentifier(), USERNAME_HASH_1, maybeAccount.get(), firstAccount);
// throw an error if second account tries to reserve or confirm the same username hash
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(secondAccount, USERNAME_HASH_1, Duration.ofDays(1)));
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(secondAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(secondAccount, USERNAME_HASH_1, Duration.ofDays(1)));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.confirmUsernameHash(secondAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
// throw an error if first account tries to reserve or confirm the username hash that it has already confirmed
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1)));
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1)));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThat(secondAccount.getReservedUsernameHash()).isEmpty();
assertThat(secondAccount.getUsernameHash()).isEmpty();
@@ -1110,12 +1109,12 @@ class AccountsTest {
void testReserveUsernameHashTransactionConflict(final Optional<String> constraintCancellationString,
final Optional<String> accountsCancellationString,
final Class<Exception> expectedException) {
final DynamoDbAsyncClient dbAsyncClient = mock(DynamoDbAsyncClient.class);
final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class);
accounts = new Accounts(
clock,
mock(DynamoDbClient.class),
dbAsyncClient,
dynamoDbClient,
mock(DynamoDbAsyncClient.class),
Tables.ACCOUNTS.tableName(),
Tables.NUMBERS.tableName(),
Tables.PNI_ASSIGNMENTS.tableName(),
@@ -1133,13 +1132,13 @@ class AccountsTest {
reason -> CancellationReason.builder().code(reason).build()
).orElse(CancellationReason.builder().build());
when(dbAsyncClient.transactWriteItems(any(TransactWriteItemsRequest.class)))
.thenReturn(CompletableFuture.failedFuture(TransactionCanceledException.builder()
when(dynamoDbClient.transactWriteItems(any(TransactWriteItemsRequest.class)))
.thenThrow(TransactionCanceledException.builder()
.cancellationReasons(constraintCancellationReason, accountsCancellationReason)
.build()));
.build());
CompletableFutureTestUtil.assertFailsWithCause(expectedException,
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
assertThrows(expectedException,
() -> accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
}
private static Stream<Arguments> testReserveUsernameHashTransactionConflict() {
@@ -1156,12 +1155,12 @@ class AccountsTest {
void testConfirmUsernameHashTransactionConflict(final Optional<String> constraintCancellationString,
final Optional<String> accountsCancellationString,
final Class<Exception> expectedException) {
final DynamoDbAsyncClient dbAsyncClient = mock(DynamoDbAsyncClient.class);
final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class);
accounts = new Accounts(
clock,
mock(DynamoDbClient.class),
dbAsyncClient,
dynamoDbClient,
mock(DynamoDbAsyncClient.class),
Tables.ACCOUNTS.tableName(),
Tables.NUMBERS.tableName(),
Tables.PNI_ASSIGNMENTS.tableName(),
@@ -1179,15 +1178,15 @@ class AccountsTest {
reason -> CancellationReason.builder().code(reason).build()
).orElse(CancellationReason.builder().build());
when(dbAsyncClient.transactWriteItems(any(TransactWriteItemsRequest.class)))
.thenReturn(CompletableFuture.failedFuture(TransactionCanceledException.builder()
when(dynamoDbClient.transactWriteItems(any(TransactWriteItemsRequest.class)))
.thenThrow(TransactionCanceledException.builder()
.cancellationReasons(constraintCancellationReason,
accountsCancellationReason,
CancellationReason.builder().build())
.build()));
.build());
CompletableFutureTestUtil.assertFailsWithCause(expectedException,
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThrows(expectedException,
() -> accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
}
private static Stream<Arguments> testConfirmUsernameHashTransactionConflict() {
@@ -1199,28 +1198,28 @@ class AccountsTest {
}
@Test
void testConfirmUsernameHashVersionMismatch() {
void testConfirmUsernameHashVersionMismatch() throws UsernameHashNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
account.setVersion(account.getVersion() + 77);
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThat(account.getUsernameHash()).isEmpty();
}
@Test
void testClearUsername() {
void testClearUsername() throws UsernameHashNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isPresent();
accounts.clearUsernameHash(account).join();
accounts.clearUsernameHash(account);
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
assertThat(accounts.getByAccountIdentifier(account.getUuid()))
@@ -1236,21 +1235,21 @@ class AccountsTest {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
assertThatNoException().isThrownBy(() -> accounts.clearUsernameHash(account).join());
assertThatNoException().isThrownBy(() -> accounts.clearUsernameHash(account));
}
@Test
void testClearUsernameVersionMismatch() {
void testClearUsernameVersionMismatch() throws UsernameHashNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
account.setVersion(account.getVersion() + 12);
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
accounts.clearUsernameHash(account));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.clearUsernameHash(account));
assertArrayEquals(USERNAME_HASH_1, account.getUsernameHash().orElseThrow());
}
@@ -1259,13 +1258,13 @@ class AccountsTest {
@ParameterizedTest
@MethodSource
void testClearUsernameTransactionConflict(final Optional<String> constraintCancellationString,
final Optional<String> accountsCancellationString) {
final DynamoDbAsyncClient dbAsyncClient = mock(DynamoDbAsyncClient.class);
final Optional<String> accountsCancellationString) throws UsernameHashNotAvailableException {
final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class);
accounts = new Accounts(
clock,
mock(DynamoDbClient.class),
dbAsyncClient,
dynamoDbClient,
mock(DynamoDbAsyncClient.class),
Tables.ACCOUNTS.tableName(),
Tables.NUMBERS.tableName(),
Tables.PNI_ASSIGNMENTS.tableName(),
@@ -1276,27 +1275,27 @@ class AccountsTest {
final Account account = generateAccount("+14155551111", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
when(dbAsyncClient.transactWriteItems(any(TransactWriteItemsRequest.class)))
.thenReturn(CompletableFuture.completedFuture(mock(TransactWriteItemsResponse.class)));
when(dynamoDbClient.transactWriteItems(any(TransactWriteItemsRequest.class)))
.thenReturn(mock(TransactWriteItemsResponse.class));
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
final CancellationReason constraintCancellationReason = constraintCancellationString.map(
reason -> CancellationReason.builder().code(reason).build()
).orElse(CancellationReason.builder().build());
final CancellationReason constraintCancellationReason = constraintCancellationString
.map(reason -> CancellationReason.builder().code(reason).build())
.orElse(CancellationReason.builder().build());
final CancellationReason accountsCancellationReason = accountsCancellationString.map(
reason -> CancellationReason.builder().code(reason).build()
).orElse(CancellationReason.builder().build());
final CancellationReason accountsCancellationReason = accountsCancellationString
.map(reason -> CancellationReason.builder().code(reason).build())
.orElse(CancellationReason.builder().build());
when(dbAsyncClient.transactWriteItems(any(TransactWriteItemsRequest.class)))
.thenReturn(CompletableFuture.failedFuture(TransactionCanceledException.builder()
when(dynamoDbClient.transactWriteItems(any(TransactWriteItemsRequest.class)))
.thenThrow(TransactionCanceledException.builder()
.cancellationReasons(accountsCancellationReason, constraintCancellationReason)
.build()));
.build());
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
accounts.clearUsernameHash(account));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.clearUsernameHash(account));
assertArrayEquals(USERNAME_HASH_1, account.getUsernameHash().orElseThrow());
}
@@ -1309,24 +1308,24 @@ class AccountsTest {
}
@Test
void testReservedUsernameHash() {
void testReservedUsernameHash() throws UsernameHashNotAvailableException {
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
createAccount(account1);
final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID());
createAccount(account2);
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1));
assertArrayEquals(USERNAME_HASH_1, account1.getReservedUsernameHash().orElseThrow());
assertThat(account1.getUsernameHash()).isEmpty();
// account 2 shouldn't be able to reserve or confirm the same username hash
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
assertThat(account1.getReservedUsernameHash()).isEmpty();
assertArrayEquals(USERNAME_HASH_1, account1.getUsernameHash().orElseThrow());
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().orElseThrow().getUuid()).isEqualTo(account1.getUuid());
@@ -1338,15 +1337,15 @@ class AccountsTest {
}
@Test
void switchBetweenReservedUsernameHashes() {
void switchBetweenReservedUsernameHashes() throws UsernameHashNotAvailableException {
final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
assertArrayEquals(USERNAME_HASH_1, account.getReservedUsernameHash().orElseThrow());
assertThat(account.getUsernameHash()).isEmpty();
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1));
assertArrayEquals(USERNAME_HASH_2, account.getReservedUsernameHash().orElseThrow());
assertThat(account.getUsernameHash()).isEmpty();
@@ -1359,7 +1358,7 @@ class AccountsTest {
clock.pin(Instant.EPOCH.plus(Duration.ofMinutes(1)));
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
assertArrayEquals(USERNAME_HASH_1, account.getReservedUsernameHash().orElseThrow());
assertThat(account.getUsernameHash()).isEmpty();
@@ -1371,23 +1370,23 @@ class AccountsTest {
}
@Test
void reserveOwnConfirmedUsername() {
void reserveOwnConfirmedUsername() throws UsernameHashNotAvailableException {
final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
assertArrayEquals(USERNAME_HASH_1, account.getReservedUsernameHash().orElseThrow());
assertThat(account.getUsernameHash()).isEmpty();
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).containsKey(Accounts.UsernameTable.ATTR_TTL);
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
assertThat(account.getReservedUsernameHash()).isEmpty();
assertArrayEquals(USERNAME_HASH_1, account.getUsernameHash().orElseThrow());
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).doesNotContainKey(Accounts.UsernameTable.ATTR_TTL);
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
assertThat(account.getReservedUsernameHash()).isEmpty();
assertArrayEquals(USERNAME_HASH_1, account.getUsernameHash().orElseThrow());
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).containsKey(Accounts.UsernameTable.KEY_USERNAME_HASH);
@@ -1395,47 +1394,47 @@ class AccountsTest {
}
@Test
void testConfirmReservedUsernameHashWrongAccountUuid() {
void testConfirmReservedUsernameHashWrongAccountUuid() throws UsernameHashNotAvailableException {
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
createAccount(account1);
final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID());
createAccount(account2);
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1));
assertArrayEquals(USERNAME_HASH_1, account1.getReservedUsernameHash().orElseThrow());
assertThat(account1.getUsernameHash()).isEmpty();
// only account1 should be able to confirm the reserved hash
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
}
@Test
void testConfirmExpiredReservedUsernameHash() {
void testConfirmExpiredReservedUsernameHash() throws UsernameHashNotAvailableException {
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
createAccount(account1);
final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID());
createAccount(account2);
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2)).join();
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2));
for (int i = 0; i <= 2; i++) {
clock.pin(Instant.EPOCH.plus(Duration.ofDays(i)));
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
}
// after 2 days, can reserve and confirm the hash
clock.pin(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1)));
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1));
assertEquals(USERNAME_HASH_1, account2.getReservedUsernameHash().orElseThrow());
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2)));
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2)));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().orElseThrow().getUuid()).isEqualTo(account2.getUuid());
}
@@ -1444,30 +1443,30 @@ class AccountsTest {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
account.setVersion(account.getVersion() + 12);
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
assertThat(account.getReservedUsernameHash()).isEmpty();
assertThat(account.getUsernameHash()).isEmpty();
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
void testRemoveOldestHold(boolean clearUsername) {
void testRemoveOldestHold(boolean clearUsername) throws UsernameHashNotAvailableException {
Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
final List<byte[]> usernames = IntStream.range(0, 7).mapToObj(i -> TestRandomUtil.nextBytes(32)).toList();
final List<byte[]> usernames = IntStream.range(0, 7).mapToObj(_ -> TestRandomUtil.nextBytes(32)).toList();
final ArrayDeque<byte[]> expectedHolds = new ArrayDeque<>();
expectedHolds.add(USERNAME_HASH_1);
for (byte[] username : usernames) {
accounts.reserveUsernameHash(account, username, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, username, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, username, Duration.ofDays(1));
accounts.confirmUsernameHash(account, username, ENCRYPTED_USERNAME_1);
assertThat(accounts.getByUsernameHash(username).join()).isPresent();
final Account read = accounts.getByAccountIdentifier(account.getUuid()).orElseThrow();
@@ -1482,7 +1481,7 @@ class AccountsTest {
// clearing the username adds a hold, but the subsequent confirm in the next iteration should add the same hold
// (should be a noop) so we don't need to touch expectedHolds
if (clearUsername) {
accounts.clearUsernameHash(account).join();
accounts.clearUsernameHash(account);
}
}
@@ -1496,72 +1495,69 @@ class AccountsTest {
final List<byte[]> freeUsernames = usernames.subList(0, numFree);
final List<byte[]> heldUsernames = usernames.subList(numFree, usernames.size());
for (byte[] username : freeUsernames) {
assertDoesNotThrow(() ->
accounts.reserveUsernameHash(account2, username, Duration.ofDays(2)).join());
assertDoesNotThrow(() -> accounts.reserveUsernameHash(account2, username, Duration.ofDays(2)));
}
for (byte[] username : heldUsernames) {
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account2, username, Duration.ofDays(2)));
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(account2, username, Duration.ofDays(2)));
}
}
@Test
void testHoldUsername() {
void testHoldUsername() throws UsernameHashNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
accounts.clearUsernameHash(account).join();
accounts.clearUsernameHash(account);
Account account2 = generateAccount("+18005554321", UUID.randomUUID(), UUID.randomUUID());
createAccount(account2);
CompletableFutureTestUtil.assertFailsWithCause(
UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)),
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)),
"account2 should not be able reserve username held by account");
// but we should be able to get it back
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
}
@Test
void testNoHoldsBarred() {
void testNoHoldsBarred() throws UsernameHashNotAvailableException {
// should be able to reserve all MAX_HOLDS usernames
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
final List<byte[]> usernames = IntStream.range(0, Accounts.MAX_USERNAME_HOLDS + 1)
.mapToObj(i -> TestRandomUtil.nextBytes(32))
.mapToObj(_ -> TestRandomUtil.nextBytes(32))
.toList();
for (byte[] username : usernames) {
accounts.reserveUsernameHash(account, username, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, username, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, username, Duration.ofDays(1));
accounts.confirmUsernameHash(account, username, ENCRYPTED_USERNAME_1);
}
// someone else shouldn't be able to get any of our holds
Account account2 = generateAccount("+18005554321", UUID.randomUUID(), UUID.randomUUID());
createAccount(account2);
for (byte[] username : usernames) {
CompletableFutureTestUtil.assertFailsWithCause(
UsernameHashNotAvailableException.class,
accounts.reserveUsernameHash(account2, username, Duration.ofDays(1)),
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(account2, username, Duration.ofDays(1)),
"account2 should not be able reserve username held by account");
}
// once the hold expires it's fine though
clock.pin(Instant.EPOCH.plus(Accounts.USERNAME_HOLD_DURATION).plus(Duration.ofSeconds(1)));
accounts.reserveUsernameHash(account2, usernames.getFirst(), Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account2, usernames.getFirst(), Duration.ofDays(1));
// if account1 modifies their username, we should also clear out the old holds, leaving only their newly added hold
accounts.clearUsernameHash(account).join();
accounts.clearUsernameHash(account);
assertThat(account.getUsernameHolds().stream().map(Account.UsernameHold::usernameHash))
.containsExactly(usernames.getLast());
}
@Test
public void testCannotRemoveHold() {
public void testCannotRemoveHold() throws UsernameHashNotAvailableException {
// Tests the case where we are trying to remove a hold we think we have, but it turns out we've already lost it.
// This means that the Account record an account has a hold on a particular username, but that hold is held by
// someone else in the username table. This can happen when the hold TTL expires while we are performing the update
@@ -1569,44 +1565,44 @@ class AccountsTest {
// case, a simple retry should let us check the clock again and notice that our hold in our account has expired.
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_1);
// Now we have a hold on username_hash_1. Simulate a race where the TTL on username_hash_1 expires, and someone
// else picks up the username by going forward and then back in time
Account account2 = generateAccount("+18005554321", UUID.randomUUID(), UUID.randomUUID());
createAccount(account2);
clock.pin(Instant.EPOCH.plus(Accounts.USERNAME_HOLD_DURATION).plus(Duration.ofSeconds(1)));
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
clock.pin(Instant.EPOCH);
// already have 1 hold, should be able to get to MAX_HOLDS without a problem
for (int i = 1; i < Accounts.MAX_USERNAME_HOLDS; i++) {
accounts.reserveUsernameHash(account, TestRandomUtil.nextBytes(32), Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, TestRandomUtil.nextBytes(32), ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, TestRandomUtil.nextBytes(32), Duration.ofDays(1));
accounts.confirmUsernameHash(account, TestRandomUtil.nextBytes(32), ENCRYPTED_USERNAME_1);
}
accounts.reserveUsernameHash(account, TestRandomUtil.nextBytes(32), Duration.ofDays(1)).join();
accounts.reserveUsernameHash(account, TestRandomUtil.nextBytes(32), Duration.ofDays(1));
// Should fail, because we cannot remove our hold on USERNAME_HASH_1
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
accounts.confirmUsernameHash(account, TestRandomUtil.nextBytes(32), ENCRYPTED_USERNAME_1));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.confirmUsernameHash(account, TestRandomUtil.nextBytes(32), ENCRYPTED_USERNAME_1));
// Should now pass once we realize our hold's TTL is over
clock.pin(Instant.EPOCH.plus(Accounts.USERNAME_HOLD_DURATION).plus(Duration.ofSeconds(1)));
accounts.confirmUsernameHash(account, TestRandomUtil.nextBytes(32), ENCRYPTED_USERNAME_1).join();
accounts.confirmUsernameHash(account, TestRandomUtil.nextBytes(32), ENCRYPTED_USERNAME_1);
}
@Test
void testDeduplicateHoldsOnSwappedUsernames() {
void testDeduplicateHoldsOnSwappedUsernames() throws UsernameHashNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
final Consumer<byte[]> assertSingleHold = (byte[] usernameToCheck) -> {
// our account should have exactly one hold for the username
@@ -1623,25 +1619,25 @@ class AccountsTest {
// Swap back and forth between username 1 and 2. Username hashes shouldn't reappear in our holds if we already have
// a hold
for (int i = 0; i < 5; i++) {
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofSeconds(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofSeconds(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_1);
assertSingleHold.accept(USERNAME_HASH_1);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofSeconds(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofSeconds(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
assertSingleHold.accept(USERNAME_HASH_2);
}
}
@Test
void testRemoveHoldAfterConfirm() {
void testRemoveHoldAfterConfirm() throws UsernameHashNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
createAccount(account);
final List<byte[]> usernames = IntStream.range(0, Accounts.MAX_USERNAME_HOLDS)
.mapToObj(i -> TestRandomUtil.nextBytes(32)).toList();
.mapToObj(_ -> TestRandomUtil.nextBytes(32)).toList();
for (byte[] username : usernames) {
accounts.reserveUsernameHash(account, username, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, username, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, username, Duration.ofDays(1));
accounts.confirmUsernameHash(account, username, ENCRYPTED_USERNAME_1);
}
int holdToRereserve = (Accounts.MAX_USERNAME_HOLDS / 2) - 1;
@@ -1651,8 +1647,8 @@ class AccountsTest {
.containsExactlyElementsOf(usernames.subList(0, usernames.size() - 1));
// if we confirm a username we already have held, it should just drop out of the holds list
accounts.reserveUsernameHash(account, usernames.get(holdToRereserve), Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, usernames.get(holdToRereserve), ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, usernames.get(holdToRereserve), Duration.ofDays(1));
accounts.confirmUsernameHash(account, usernames.get(holdToRereserve), ENCRYPTED_USERNAME_1);
// should have a hold on every username but the one we just confirmed
assertThat(account.getUsernameHolds().stream().map(Account.UsernameHold::usernameHash).toList())
@@ -1682,7 +1678,7 @@ class AccountsTest {
}
@Test
void testGetByUsernameHashAsync() {
void testGetByUsernameHashAsync() throws UsernameHashNotAvailableException {
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
@@ -1690,8 +1686,8 @@ class AccountsTest {
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isPresent();
}
@@ -1764,20 +1760,16 @@ class AccountsTest {
final Account conflictingUsernameAccount = nextRandomAccount();
createAccount(conflictingUsernameAccount);
final CompletionException completionException = assertThrows(CompletionException.class,
() -> accounts.reserveUsernameHash(conflictingUsernameAccount, USERNAME_HASH_1, Accounts.USERNAME_HOLD_DURATION).join());
assertInstanceOf(UsernameHashNotAvailableException.class, completionException.getCause());
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(conflictingUsernameAccount, USERNAME_HASH_1, Accounts.USERNAME_HOLD_DURATION));
}
{
final Account conflictingUsernameHoldAccount = nextRandomAccount();
createAccount(conflictingUsernameHoldAccount);
final CompletionException completionException = assertThrows(CompletionException.class,
() -> accounts.reserveUsernameHash(conflictingUsernameHoldAccount, USERNAME_HASH_2, Accounts.USERNAME_HOLD_DURATION).join());
assertInstanceOf(UsernameHashNotAvailableException.class, completionException.getCause());
assertThrows(UsernameHashNotAvailableException.class,
() -> accounts.reserveUsernameHash(conflictingUsernameHoldAccount, USERNAME_HASH_2, Accounts.USERNAME_HOLD_DURATION));
}
// Check that bare constraint records are written as expected
@@ -1795,7 +1787,7 @@ class AccountsTest {
}
@Test
void testRegeneratedConstraintsMatchOriginalConstraints() {
void testRegeneratedConstraintsMatchOriginalConstraints() throws UsernameHashNotAvailableException {
final Instant usernameHoldExpiration = clock.instant().plus(Accounts.USERNAME_HOLD_DURATION).truncatedTo(ChronoUnit.SECONDS);
final Account account = nextRandomAccount();
@@ -1804,10 +1796,10 @@ class AccountsTest {
account.setUsernameHolds(List.of(new Account.UsernameHold(USERNAME_HASH_2, usernameHoldExpiration.getEpochSecond())));
createAccount(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Accounts.USERNAME_HOLD_DURATION).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Accounts.USERNAME_HOLD_DURATION).join();
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Accounts.USERNAME_HOLD_DURATION);
accounts.confirmUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Accounts.USERNAME_HOLD_DURATION);
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1);
final Map<String, AttributeValue> originalE164ConstraintItem =
DYNAMO_DB_EXTENSION.getDynamoDbClient().getItem(GetItemRequest.builder()