Remove two-stage check of username availability in reserve/confirm
This commit is contained in:
parent
ed972a0037
commit
184cdc0331
|
@ -439,6 +439,9 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reserve a username hash under the account UUID
|
* Reserve a username hash under the account UUID
|
||||||
|
* @return a future that completes once the username hash has been reserved; may fail with an
|
||||||
|
* {@link ContestedOptimisticLockException} if the account has been updated or
|
||||||
|
* {@link UsernameHashNotAvailableException} if the username was taken by someone else
|
||||||
*/
|
*/
|
||||||
public CompletableFuture<Void> reserveUsernameHash(
|
public CompletableFuture<Void> reserveUsernameHash(
|
||||||
final Account account,
|
final Account account,
|
||||||
|
@ -504,7 +507,12 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||||
.build())
|
.build())
|
||||||
.exceptionally(throwable -> {
|
.exceptionally(throwable -> {
|
||||||
if (ExceptionUtils.unwrap(throwable) instanceof TransactionCanceledException e) {
|
if (ExceptionUtils.unwrap(throwable) instanceof TransactionCanceledException e) {
|
||||||
if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch(CONDITIONAL_CHECK_FAILED::equals)) {
|
// If the constraint table update failed the condition check, the username's taken and we should stop
|
||||||
|
// trying. However if it was only in the accounts table that the condition check update failed, it's an
|
||||||
|
// optimistic locking failure (the account was concurrently updated) and we should try again.
|
||||||
|
if (conditionalCheckFailed(e.cancellationReasons().get(0))) {
|
||||||
|
throw ExceptionUtils.wrap(new UsernameHashNotAvailableException());
|
||||||
|
} else if (conditionalCheckFailed(e.cancellationReasons().get(1))) {
|
||||||
throw new ContestedOptimisticLockException();
|
throw new ContestedOptimisticLockException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -529,7 +537,8 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||||
* @param account to update
|
* @param account to update
|
||||||
* @param usernameHash believed to be available
|
* @param usernameHash believed to be available
|
||||||
* @return a future that completes once the username hash has been confirmed; may fail with an
|
* @return a future that completes once the username hash has been confirmed; may fail with an
|
||||||
* {@link ContestedOptimisticLockException} if the account has been updated or the username has taken by someone else
|
* {@link ContestedOptimisticLockException} if the account has been updated or
|
||||||
|
* {@link UsernameHashNotAvailableException} if the username was taken by someone else
|
||||||
*/
|
*/
|
||||||
public CompletableFuture<Void> confirmUsernameHash(final Account account, final byte[] usernameHash, @Nullable final byte[] encryptedUsername) {
|
public CompletableFuture<Void> confirmUsernameHash(final Account account, final byte[] usernameHash, @Nullable final byte[] encryptedUsername) {
|
||||||
final Timer.Sample sample = Timer.start();
|
final Timer.Sample sample = Timer.start();
|
||||||
|
@ -559,10 +568,15 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||||
return (Void) null;
|
return (Void) null;
|
||||||
})
|
})
|
||||||
.exceptionally(throwable -> {
|
.exceptionally(throwable -> {
|
||||||
if (ExceptionUtils.unwrap(
|
if (ExceptionUtils.unwrap(throwable) instanceof TransactionCanceledException e) {
|
||||||
throwable) instanceof TransactionCanceledException transactionCanceledException) {
|
// If the constraint table update failed the condition check, the username's taken and we should stop
|
||||||
if (transactionCanceledException.cancellationReasons().stream().map(CancellationReason::code)
|
// trying. However if it was only in the accounts table that the condition check update failed, it's an
|
||||||
.anyMatch(CONDITIONAL_CHECK_FAILED::equals)) {
|
// optimistic locking failure (the account was concurrently updated) and we should try again.
|
||||||
|
// NOTE: the fixed indices here must be kept in sync with the creation of the TransactWriteItems in
|
||||||
|
// buildConfirmUsernameHashRequest!
|
||||||
|
if (conditionalCheckFailed(e.cancellationReasons().get(0))) {
|
||||||
|
throw ExceptionUtils.wrap(new UsernameHashNotAvailableException());
|
||||||
|
} else if (conditionalCheckFailed(e.cancellationReasons().get(1))) {
|
||||||
throw new ContestedOptimisticLockException();
|
throw new ContestedOptimisticLockException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -603,6 +617,8 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||||
final byte[] usernameHash = updatedAccount.getUsernameHash()
|
final byte[] usernameHash = updatedAccount.getUsernameHash()
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Account must have a username hash"));
|
.orElseThrow(() -> new IllegalArgumentException("Account must have a username hash"));
|
||||||
|
|
||||||
|
// NOTE: the order in which writeItems are added to the list is significant, and must be kept in sync with the catch block in confirmUsernameHash!
|
||||||
|
|
||||||
// add the username hash to the constraint table, wiping out the ttl if we had already reserved the hash
|
// add the username hash to the constraint table, wiping out the ttl if we had already reserved the hash
|
||||||
writeItems.add(TransactWriteItem.builder()
|
writeItems.add(TransactWriteItem.builder()
|
||||||
.put(Put.builder()
|
.put(Put.builder()
|
||||||
|
@ -908,28 +924,6 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Boolean> usernameHashAvailable(final byte[] username) {
|
|
||||||
return usernameHashAvailable(Optional.empty(), username);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<Boolean> usernameHashAvailable(final Optional<UUID> accountUuid, final byte[] usernameHash) {
|
|
||||||
return itemByKeyAsync(usernamesConstraintTableName, ATTR_USERNAME_HASH, AttributeValues.fromByteArray(usernameHash))
|
|
||||||
.thenApply(maybeUsernameHashItem -> maybeUsernameHashItem
|
|
||||||
.map(item -> {
|
|
||||||
if (AttributeValues.getLong(item, ATTR_TTL, Long.MAX_VALUE) < clock.instant().getEpochSecond()) {
|
|
||||||
// username hash was reserved, but has expired
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// username hash is reserved by us
|
|
||||||
return !AttributeValues.getBool(item, ATTR_CONFIRMED, true) && accountUuid
|
|
||||||
.map(AttributeValues.getUUID(item, KEY_ACCOUNT_UUID, new UUID(0, 0))::equals)
|
|
||||||
.orElse(false);
|
|
||||||
})
|
|
||||||
// If no item was found, then the username hash is free
|
|
||||||
.orElse(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public Optional<Account> getByE164(final String number) {
|
public Optional<Account> getByE164(final String number) {
|
||||||
return getByIndirectLookup(
|
return getByIndirectLookup(
|
||||||
|
|
|
@ -579,23 +579,17 @@ public class AccountsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<byte[]> checkAndReserveNextUsernameHash(final Account account, final Queue<byte[]> requestedUsernameHashes) {
|
private CompletableFuture<byte[]> checkAndReserveNextUsernameHash(final Account account, final Queue<byte[]> requestedUsernameHashes) {
|
||||||
final byte[] usernameHash;
|
final byte[] usernameHash = requestedUsernameHashes.remove();
|
||||||
|
|
||||||
try {
|
return accounts.reserveUsernameHash(account, usernameHash, USERNAME_HASH_RESERVATION_TTL_MINUTES)
|
||||||
usernameHash = requestedUsernameHashes.remove();
|
.thenApply(ignored -> usernameHash)
|
||||||
} catch (final NoSuchElementException e) {
|
.exceptionallyComposeAsync(
|
||||||
return CompletableFuture.failedFuture(new UsernameHashNotAvailableException());
|
throwable -> {
|
||||||
}
|
if (ExceptionUtils.unwrap(throwable) instanceof UsernameHashNotAvailableException && !requestedUsernameHashes.isEmpty()) {
|
||||||
|
return checkAndReserveNextUsernameHash(account, requestedUsernameHashes);
|
||||||
return accounts.usernameHashAvailable(usernameHash)
|
}
|
||||||
.thenCompose(usernameHashAvailable -> {
|
return CompletableFuture.failedFuture(throwable);
|
||||||
if (usernameHashAvailable) {
|
});
|
||||||
return accounts.reserveUsernameHash(account, usernameHash, USERNAME_HASH_RESERVATION_TTL_MINUTES)
|
|
||||||
.thenApply(ignored -> usernameHash);
|
|
||||||
} else {
|
|
||||||
return checkAndReserveNextUsernameHash(account, requestedUsernameHashes);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -629,14 +623,7 @@ public class AccountsManager {
|
||||||
.thenCompose(ignored -> updateWithRetriesAsync(
|
.thenCompose(ignored -> updateWithRetriesAsync(
|
||||||
account,
|
account,
|
||||||
a -> true,
|
a -> true,
|
||||||
a -> accounts.usernameHashAvailable(Optional.of(account.getUuid()), reservedUsernameHash)
|
a -> accounts.confirmUsernameHash(a, reservedUsernameHash, encryptedUsername),
|
||||||
.thenCompose(usernameHashAvailable -> {
|
|
||||||
if (!usernameHashAvailable) {
|
|
||||||
return CompletableFuture.failedFuture(new UsernameHashNotAvailableException());
|
|
||||||
}
|
|
||||||
|
|
||||||
return accounts.confirmUsernameHash(a, reservedUsernameHash, encryptedUsername);
|
|
||||||
}),
|
|
||||||
() -> accounts.getByAccountIdentifierAsync(account.getUuid()).thenApply(Optional::orElseThrow),
|
() -> accounts.getByAccountIdentifierAsync(account.getUuid()).thenApply(Optional::orElseThrow),
|
||||||
AccountChangeValidator.USERNAME_CHANGE_VALIDATOR,
|
AccountChangeValidator.USERNAME_CHANGE_VALIDATOR,
|
||||||
MAX_UPDATE_ATTEMPTS
|
MAX_UPDATE_ATTEMPTS
|
||||||
|
|
|
@ -77,6 +77,7 @@ import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||||
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager.UsernameReservation;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.DevicesHelper;
|
import org.whispersystems.textsecuregcm.tests.util.DevicesHelper;
|
||||||
|
@ -86,6 +87,7 @@ import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
|
||||||
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
|
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
|
||||||
import org.whispersystems.textsecuregcm.util.Pair;
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
import org.whispersystems.textsecuregcm.util.TestClock;
|
import org.whispersystems.textsecuregcm.util.TestClock;
|
||||||
|
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||||
|
|
||||||
@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
|
@Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
|
||||||
class AccountsManagerTest {
|
class AccountsManagerTest {
|
||||||
|
@ -193,7 +195,6 @@ class AccountsManagerTest {
|
||||||
|
|
||||||
enrollmentManager = mock(ExperimentEnrollmentManager.class);
|
enrollmentManager = mock(ExperimentEnrollmentManager.class);
|
||||||
when(enrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))).thenReturn(true);
|
when(enrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))).thenReturn(true);
|
||||||
when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(true));
|
|
||||||
|
|
||||||
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
|
final AccountLockManager accountLockManager = mock(AccountLockManager.class);
|
||||||
|
|
||||||
|
@ -1587,18 +1588,36 @@ class AccountsManagerTest {
|
||||||
@Test
|
@Test
|
||||||
void testReserveUsernameHash() throws UsernameHashNotAvailableException {
|
void testReserveUsernameHash() throws UsernameHashNotAvailableException {
|
||||||
final 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]);
|
||||||
final List<byte[]> usernameHashes = List.of(new byte[32], new byte[32]);
|
when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
|
||||||
when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(true));
|
|
||||||
|
final List<byte[]> usernameHashes = List.of(TestRandomUtil.nextBytes(32), TestRandomUtil.nextBytes(32));
|
||||||
when(accounts.reserveUsernameHash(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
when(accounts.reserveUsernameHash(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||||
accountsManager.reserveUsernameHash(account, usernameHashes);
|
|
||||||
verify(accounts).reserveUsernameHash(eq(account), eq(new byte[32]), eq(Duration.ofMinutes(5)));
|
UsernameReservation result = accountsManager.reserveUsernameHash(account, usernameHashes).join();
|
||||||
|
assertArrayEquals(usernameHashes.get(0), result.reservedUsernameHash());
|
||||||
|
verify(accounts, times(1)).reserveUsernameHash(eq(account), any(), eq(Duration.ofMinutes(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
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)));
|
||||||
|
|
||||||
|
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());
|
||||||
|
verify(accounts, times(2)).reserveUsernameHash(eq(account), any(), eq(Duration.ofMinutes(5)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testReserveUsernameHashNotAvailable() {
|
void testReserveUsernameHashNotAvailable() {
|
||||||
final 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]);
|
||||||
when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(false));
|
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,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1, USERNAME_HASH_2)));
|
accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1, USERNAME_HASH_2)));
|
||||||
}
|
}
|
||||||
|
@ -1615,8 +1634,6 @@ class AccountsManagerTest {
|
||||||
void testConfirmReservedUsernameHash() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
void testConfirmReservedUsernameHash() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
|
||||||
final 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]);
|
||||||
setReservationHash(account, USERNAME_HASH_1);
|
setReservationHash(account, USERNAME_HASH_1);
|
||||||
when(accounts.usernameHashAvailable(Optional.of(account.getUuid()), USERNAME_HASH_1))
|
|
||||||
.thenReturn(CompletableFuture.completedFuture(true));
|
|
||||||
|
|
||||||
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
|
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
|
||||||
.thenReturn(CompletableFuture.completedFuture(null));
|
.thenReturn(CompletableFuture.completedFuture(null));
|
||||||
|
@ -1625,12 +1642,26 @@ class AccountsManagerTest {
|
||||||
verify(accounts).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1));
|
verify(accounts).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
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.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
|
||||||
|
.thenReturn(CompletableFuture.failedFuture(new ContestedOptimisticLockException()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(null));
|
||||||
|
|
||||||
|
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
|
||||||
|
verify(accounts, times(2)).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1), eq(ENCRYPTED_USERNAME_1));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConfirmReservedHashNameMismatch() {
|
void testConfirmReservedHashNameMismatch() {
|
||||||
final 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]);
|
||||||
setReservationHash(account, USERNAME_HASH_1);
|
setReservationHash(account, USERNAME_HASH_1);
|
||||||
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1)))
|
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
|
||||||
.thenReturn(CompletableFuture.completedFuture(true));
|
.thenReturn(CompletableFuture.completedFuture(null));
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(UsernameReservationNotFoundException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameReservationNotFoundException.class,
|
||||||
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2));
|
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_2, ENCRYPTED_USERNAME_2));
|
||||||
}
|
}
|
||||||
|
@ -1640,11 +1671,11 @@ class AccountsManagerTest {
|
||||||
final 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]);
|
||||||
// hash was reserved, but the reservation lapsed and another account took it
|
// hash was reserved, but the reservation lapsed and another account took it
|
||||||
setReservationHash(account, USERNAME_HASH_1);
|
setReservationHash(account, USERNAME_HASH_1);
|
||||||
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1)))
|
when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1))
|
||||||
.thenReturn(CompletableFuture.completedFuture(false));
|
.thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException()));
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
||||||
verify(accounts, never()).confirmUsernameHash(any(), any(), any());
|
assertTrue(account.getUsernameHash().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -182,7 +182,7 @@ class AccountsManagerUsernameIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testReserveUsernameSnatched() throws InterruptedException, UsernameHashNotAvailableException {
|
void testReserveUsernameGetFirstAvailableChoice() throws InterruptedException, UsernameHashNotAvailableException {
|
||||||
final Account account = AccountsHelper.createAccount(accountsManager, "+18005551111");
|
final Account account = AccountsHelper.createAccount(accountsManager, "+18005551111");
|
||||||
|
|
||||||
ArrayList<byte[]> usernameHashes = new ArrayList<>(Arrays.asList(USERNAME_HASH_1, USERNAME_HASH_2));
|
ArrayList<byte[]> usernameHashes = new ArrayList<>(Arrays.asList(USERNAME_HASH_1, USERNAME_HASH_2));
|
||||||
|
@ -198,23 +198,14 @@ class AccountsManagerUsernameIntegrationTest {
|
||||||
|
|
||||||
byte[] availableHash = TestRandomUtil.nextBytes(32);
|
byte[] availableHash = TestRandomUtil.nextBytes(32);
|
||||||
usernameHashes.add(availableHash);
|
usernameHashes.add(availableHash);
|
||||||
|
usernameHashes.add(TestRandomUtil.nextBytes(32));
|
||||||
|
|
||||||
// first time this is called lie and say the username is available
|
|
||||||
// this simulates seeing an available username and then it being taken
|
|
||||||
// by someone before the write
|
|
||||||
doReturn(CompletableFuture.completedFuture(true))
|
|
||||||
.doCallRealMethod()
|
|
||||||
.when(accounts).usernameHashAvailable(any());
|
|
||||||
final byte[] username = accountsManager
|
final byte[] username = accountsManager
|
||||||
.reserveUsernameHash(account, usernameHashes)
|
.reserveUsernameHash(account, usernameHashes)
|
||||||
.join()
|
.join()
|
||||||
.reservedUsernameHash();
|
.reservedUsernameHash();
|
||||||
|
|
||||||
assertArrayEquals(username, availableHash);
|
assertArrayEquals(username, availableHash);
|
||||||
|
|
||||||
// 1 attempt on first try (returns true),
|
|
||||||
// 5 more attempts until "availableHash" returns true
|
|
||||||
verify(accounts, times(4)).usernameHashAvailable(any());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -945,15 +945,15 @@ class AccountsTest {
|
||||||
verifyStoredState(firstAccount.getNumber(), firstAccount.getUuid(), firstAccount.getPhoneNumberIdentifier(), USERNAME_HASH_1, maybeAccount.get(), firstAccount);
|
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
|
// throw an error if second account tries to reserve or confirm the same username hash
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.reserveUsernameHash(secondAccount, USERNAME_HASH_1, Duration.ofDays(1)));
|
accounts.reserveUsernameHash(secondAccount, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.confirmUsernameHash(secondAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
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
|
// throw an error if first account tries to reserve or confirm the username hash that it has already confirmed
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1)));
|
accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
||||||
|
|
||||||
assertThat(secondAccount.getReservedUsernameHash()).isEmpty();
|
assertThat(secondAccount.getReservedUsernameHash()).isEmpty();
|
||||||
|
@ -1029,9 +1029,9 @@ class AccountsTest {
|
||||||
assertThat(account1.getUsernameHash()).isEmpty();
|
assertThat(account1.getUsernameHash()).isEmpty();
|
||||||
|
|
||||||
// account 2 shouldn't be able to reserve or confirm the same username hash
|
// account 2 shouldn't be able to reserve or confirm the same username hash
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
|
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
||||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
|
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
|
||||||
|
|
||||||
|
@ -1095,7 +1095,7 @@ class AccountsTest {
|
||||||
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||||
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).doesNotContainKey(Accounts.ATTR_TTL);
|
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).doesNotContainKey(Accounts.ATTR_TTL);
|
||||||
|
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
|
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||||
assertThat(account.getReservedUsernameHash()).isEmpty();
|
assertThat(account.getReservedUsernameHash()).isEmpty();
|
||||||
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||||
|
@ -1103,24 +1103,6 @@ class AccountsTest {
|
||||||
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).doesNotContainKey(Accounts.ATTR_TTL);
|
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).doesNotContainKey(Accounts.ATTR_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testUsernameHashAvailable() {
|
|
||||||
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
|
||||||
createAccount(account1);
|
|
||||||
|
|
||||||
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1)).join();
|
|
||||||
assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1).join()).isFalse();
|
|
||||||
assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1).join()).isFalse();
|
|
||||||
assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1).join()).isFalse();
|
|
||||||
assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1).join()).isTrue();
|
|
||||||
|
|
||||||
accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
|
|
||||||
assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1).join()).isFalse();
|
|
||||||
assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1).join()).isFalse();
|
|
||||||
assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1).join()).isFalse();
|
|
||||||
assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1).join()).isFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConfirmReservedUsernameHashWrongAccountUuid() {
|
void testConfirmReservedUsernameHashWrongAccountUuid() {
|
||||||
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||||
|
@ -1133,7 +1115,7 @@ class AccountsTest {
|
||||||
assertThat(account1.getUsernameHash()).isEmpty();
|
assertThat(account1.getUsernameHash()).isEmpty();
|
||||||
|
|
||||||
// only account1 should be able to confirm the reserved hash
|
// only account1 should be able to confirm the reserved hash
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1148,7 +1130,7 @@ class AccountsTest {
|
||||||
|
|
||||||
for (int i = 0; i <= 2; i++) {
|
for (int i = 0; i <= 2; i++) {
|
||||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(i)));
|
clock.pin(Instant.EPOCH.plus(Duration.ofDays(i)));
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
|
accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1159,9 +1141,9 @@ class AccountsTest {
|
||||||
|
|
||||||
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
|
accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
|
||||||
|
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2)));
|
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2)));
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class,
|
||||||
accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1));
|
||||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account2.getUuid());
|
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account2.getUuid());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue