diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java index 39a7f7d64..fab835349 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java @@ -439,6 +439,9 @@ public class Accounts extends AbstractDynamoDbStore { /** * 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 reserveUsernameHash( final Account account, @@ -504,7 +507,12 @@ public class Accounts extends AbstractDynamoDbStore { .build()) .exceptionally(throwable -> { 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(); } } @@ -529,7 +537,8 @@ public class Accounts extends AbstractDynamoDbStore { * @param account to update * @param usernameHash believed to be available * @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 confirmUsernameHash(final Account account, final byte[] usernameHash, @Nullable final byte[] encryptedUsername) { final Timer.Sample sample = Timer.start(); @@ -559,10 +568,15 @@ public class Accounts extends AbstractDynamoDbStore { return (Void) null; }) .exceptionally(throwable -> { - if (ExceptionUtils.unwrap( - throwable) instanceof TransactionCanceledException transactionCanceledException) { - if (transactionCanceledException.cancellationReasons().stream().map(CancellationReason::code) - .anyMatch(CONDITIONAL_CHECK_FAILED::equals)) { + if (ExceptionUtils.unwrap(throwable) instanceof TransactionCanceledException e) { + // 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. + // 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(); } } @@ -603,6 +617,8 @@ public class Accounts extends AbstractDynamoDbStore { final byte[] usernameHash = updatedAccount.getUsernameHash() .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 writeItems.add(TransactWriteItem.builder() .put(Put.builder() @@ -908,28 +924,6 @@ public class Accounts extends AbstractDynamoDbStore { }); } - public CompletableFuture usernameHashAvailable(final byte[] username) { - return usernameHashAvailable(Optional.empty(), username); - } - - public CompletableFuture usernameHashAvailable(final Optional 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 public Optional getByE164(final String number) { return getByIndirectLookup( diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java index 9cdaa1866..d0acb7a22 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -579,23 +579,17 @@ public class AccountsManager { } private CompletableFuture checkAndReserveNextUsernameHash(final Account account, final Queue requestedUsernameHashes) { - final byte[] usernameHash; + final byte[] usernameHash = requestedUsernameHashes.remove(); - try { - usernameHash = requestedUsernameHashes.remove(); - } catch (final NoSuchElementException e) { - return CompletableFuture.failedFuture(new UsernameHashNotAvailableException()); - } - - return accounts.usernameHashAvailable(usernameHash) - .thenCompose(usernameHashAvailable -> { - if (usernameHashAvailable) { - return accounts.reserveUsernameHash(account, usernameHash, USERNAME_HASH_RESERVATION_TTL_MINUTES) - .thenApply(ignored -> usernameHash); - } else { - return checkAndReserveNextUsernameHash(account, requestedUsernameHashes); - } - }); + return accounts.reserveUsernameHash(account, usernameHash, USERNAME_HASH_RESERVATION_TTL_MINUTES) + .thenApply(ignored -> usernameHash) + .exceptionallyComposeAsync( + throwable -> { + if (ExceptionUtils.unwrap(throwable) instanceof UsernameHashNotAvailableException && !requestedUsernameHashes.isEmpty()) { + return checkAndReserveNextUsernameHash(account, requestedUsernameHashes); + } + return CompletableFuture.failedFuture(throwable); + }); } /** @@ -629,14 +623,7 @@ public class AccountsManager { .thenCompose(ignored -> updateWithRetriesAsync( account, a -> true, - a -> accounts.usernameHashAvailable(Optional.of(account.getUuid()), reservedUsernameHash) - .thenCompose(usernameHashAvailable -> { - if (!usernameHashAvailable) { - return CompletableFuture.failedFuture(new UsernameHashNotAvailableException()); - } - - return accounts.confirmUsernameHash(a, reservedUsernameHash, encryptedUsername); - }), + a -> accounts.confirmUsernameHash(a, reservedUsernameHash, encryptedUsername), () -> accounts.getByAccountIdentifierAsync(account.getUuid()).thenApply(Optional::orElseThrow), AccountChangeValidator.USERNAME_CHANGE_VALIDATOR, MAX_UPDATE_ATTEMPTS diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java index 9031c1586..98ceace50 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java @@ -77,6 +77,7 @@ import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; import org.whispersystems.textsecuregcm.push.ClientPresenceManager; import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.storage.AccountsManager.UsernameReservation; import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; 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.Pair; import org.whispersystems.textsecuregcm.util.TestClock; +import org.whispersystems.textsecuregcm.util.TestRandomUtil; @Timeout(value = 10, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) class AccountsManagerTest { @@ -193,7 +195,6 @@ class AccountsManagerTest { enrollmentManager = mock(ExperimentEnrollmentManager.class); 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); @@ -1587,18 +1588,36 @@ class AccountsManagerTest { @Test void testReserveUsernameHash() throws UsernameHashNotAvailableException { final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]); - final List usernameHashes = List.of(new byte[32], new byte[32]); - when(accounts.usernameHashAvailable(any())).thenReturn(CompletableFuture.completedFuture(true)); + when(accounts.getByAccountIdentifierAsync(account.getUuid())).thenReturn(CompletableFuture.completedFuture(Optional.of(account))); + + final List usernameHashes = List.of(TestRandomUtil.nextBytes(32), TestRandomUtil.nextBytes(32)); 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 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 void testReserveUsernameHashNotAvailable() { 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, accountsManager.reserveUsernameHash(account, List.of(USERNAME_HASH_1, USERNAME_HASH_2))); } @@ -1615,8 +1634,6 @@ class AccountsManagerTest { void testConfirmReservedUsernameHash() 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.usernameHashAvailable(Optional.of(account.getUuid()), USERNAME_HASH_1)) - .thenReturn(CompletableFuture.completedFuture(true)); when(accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)) .thenReturn(CompletableFuture.completedFuture(null)); @@ -1625,12 +1642,26 @@ class AccountsManagerTest { 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 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.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))) - .thenReturn(CompletableFuture.completedFuture(true)); + 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)); } @@ -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]); // hash was reserved, but the reservation lapsed and another account took it setReservationHash(account, USERNAME_HASH_1); - when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))) - .thenReturn(CompletableFuture.completedFuture(false)); + 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)); - verify(accounts, never()).confirmUsernameHash(any(), any(), any()); + assertTrue(account.getUsernameHash().isEmpty()); } @Test diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java index 43bbaad42..36d2cf08b 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java @@ -182,7 +182,7 @@ class AccountsManagerUsernameIntegrationTest { } @Test - void testReserveUsernameSnatched() throws InterruptedException, UsernameHashNotAvailableException { + void testReserveUsernameGetFirstAvailableChoice() throws InterruptedException, UsernameHashNotAvailableException { final Account account = AccountsHelper.createAccount(accountsManager, "+18005551111"); ArrayList usernameHashes = new ArrayList<>(Arrays.asList(USERNAME_HASH_1, USERNAME_HASH_2)); @@ -198,23 +198,14 @@ class AccountsManagerUsernameIntegrationTest { byte[] availableHash = TestRandomUtil.nextBytes(32); 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 .reserveUsernameHash(account, usernameHashes) .join() .reservedUsernameHash(); assertArrayEquals(username, availableHash); - - // 1 attempt on first try (returns true), - // 5 more attempts until "availableHash" returns true - verify(accounts, times(4)).usernameHashAvailable(any()); } @Test diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java index f1280d6d9..52bbd79a5 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsTest.java @@ -945,15 +945,15 @@ 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(ContestedOptimisticLockException.class, + CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class, 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)); // 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))); - CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class, accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); assertThat(secondAccount.getReservedUsernameHash()).isEmpty(); @@ -1029,9 +1029,9 @@ class AccountsTest { assertThat(account1.getUsernameHash()).isEmpty(); // 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))); - CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class, accounts.confirmUsernameHash(account2, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty(); @@ -1095,7 +1095,7 @@ class AccountsTest { assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1); 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))); assertThat(account.getReservedUsernameHash()).isEmpty(); assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1); @@ -1103,24 +1103,6 @@ class AccountsTest { 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 void testConfirmReservedUsernameHashWrongAccountUuid() { final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID()); @@ -1133,7 +1115,7 @@ class AccountsTest { assertThat(account1.getUsernameHash()).isEmpty(); // 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)); } @@ -1148,7 +1130,7 @@ class AccountsTest { for (int i = 0; i <= 2; 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))); } @@ -1159,9 +1141,9 @@ class AccountsTest { 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))); - CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class, + CompletableFutureTestUtil.assertFailsWithCause(UsernameHashNotAvailableException.class, accounts.confirmUsernameHash(account1, USERNAME_HASH_1, ENCRYPTED_USERNAME_1)); assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account2.getUuid()); }