mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 17:38:04 +01:00
Make username-related operations blocking
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user