Allow reserving a hash previously reserved (but not committed) by the same user
This commit is contained in:
parent
f495ff483a
commit
1e5fadc440
|
@ -474,9 +474,12 @@ public class Accounts extends AbstractDynamoDbStore {
|
||||||
ATTR_TTL, AttributeValues.fromLong(expirationTime),
|
ATTR_TTL, AttributeValues.fromLong(expirationTime),
|
||||||
ATTR_CONFIRMED, AttributeValues.fromBool(false),
|
ATTR_CONFIRMED, AttributeValues.fromBool(false),
|
||||||
ATTR_RECLAIMABLE, AttributeValues.fromBool(false)))
|
ATTR_RECLAIMABLE, AttributeValues.fromBool(false)))
|
||||||
.conditionExpression("attribute_not_exists(#username_hash) OR (#ttl < :now)")
|
.conditionExpression("attribute_not_exists(#username_hash) OR #ttl < :now OR (#aci = :aci AND #confirmed = :confirmed)")
|
||||||
.expressionAttributeNames(Map.of("#username_hash", ATTR_USERNAME_HASH, "#ttl", ATTR_TTL))
|
.expressionAttributeNames(Map.of("#username_hash", ATTR_USERNAME_HASH, "#ttl", ATTR_TTL, "#aci", KEY_ACCOUNT_UUID, "#confirmed", ATTR_CONFIRMED))
|
||||||
.expressionAttributeValues(Map.of(":now", AttributeValues.fromLong(clock.instant().getEpochSecond())))
|
.expressionAttributeValues(Map.of(
|
||||||
|
":now", AttributeValues.fromLong(clock.instant().getEpochSecond()),
|
||||||
|
":aci", AttributeValues.fromUUID(uuid),
|
||||||
|
":confirmed", AttributeValues.fromBool(false)))
|
||||||
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
|
||||||
.build())
|
.build())
|
||||||
.build());
|
.build());
|
||||||
|
|
|
@ -910,12 +910,7 @@ class AccountsTest {
|
||||||
final UUID newHandle = account.getUsernameLinkHandle();
|
final UUID newHandle = account.getUsernameLinkHandle();
|
||||||
|
|
||||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
|
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join()).isEmpty();
|
||||||
assertThat(DYNAMO_DB_EXTENSION.getDynamoDbClient()
|
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).isEmpty();
|
||||||
.getItem(GetItemRequest.builder()
|
|
||||||
.tableName(Tables.USERNAMES.tableName())
|
|
||||||
.key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1)))
|
|
||||||
.build())
|
|
||||||
.item()).isEmpty();
|
|
||||||
assertThat(accounts.getByUsernameLinkHandle(oldHandle).join()).isEmpty();
|
assertThat(accounts.getByUsernameLinkHandle(oldHandle).join()).isEmpty();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -1045,17 +1040,69 @@ class AccountsTest {
|
||||||
assertArrayEquals(account1.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
assertArrayEquals(account1.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account1.getUuid());
|
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account1.getUuid());
|
||||||
|
|
||||||
final Map<String, AttributeValue> usernameConstraintRecord = DYNAMO_DB_EXTENSION.getDynamoDbClient()
|
final Map<String, AttributeValue> usernameConstraintRecord = getUsernameConstraintTableItem(USERNAME_HASH_1);
|
||||||
.getItem(GetItemRequest.builder()
|
|
||||||
.tableName(Tables.USERNAMES.tableName())
|
|
||||||
.key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1)))
|
|
||||||
.build())
|
|
||||||
.item();
|
|
||||||
|
|
||||||
assertThat(usernameConstraintRecord).containsKey(Accounts.ATTR_USERNAME_HASH);
|
assertThat(usernameConstraintRecord).containsKey(Accounts.ATTR_USERNAME_HASH);
|
||||||
assertThat(usernameConstraintRecord).doesNotContainKey(Accounts.ATTR_TTL);
|
assertThat(usernameConstraintRecord).doesNotContainKey(Accounts.ATTR_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void switchBetweenReservedUsernameHashes() {
|
||||||
|
final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||||
|
createAccount(account);
|
||||||
|
|
||||||
|
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
|
||||||
|
assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||||
|
assertThat(account.getUsernameHash()).isEmpty();
|
||||||
|
|
||||||
|
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1)).join();
|
||||||
|
assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_2);
|
||||||
|
assertThat(account.getUsernameHash()).isEmpty();
|
||||||
|
|
||||||
|
final Map<String, AttributeValue> usernameConstraintRecord1 = getUsernameConstraintTableItem(USERNAME_HASH_1);
|
||||||
|
final Map<String, AttributeValue> usernameConstraintRecord2 = getUsernameConstraintTableItem(USERNAME_HASH_2);
|
||||||
|
assertThat(usernameConstraintRecord1).containsKey(Accounts.ATTR_USERNAME_HASH);
|
||||||
|
assertThat(usernameConstraintRecord2).containsKey(Accounts.ATTR_USERNAME_HASH);
|
||||||
|
assertThat(usernameConstraintRecord1).containsKey(Accounts.ATTR_TTL);
|
||||||
|
assertThat(usernameConstraintRecord2).containsKey(Accounts.ATTR_TTL);
|
||||||
|
|
||||||
|
clock.pin(Instant.EPOCH.plus(Duration.ofMinutes(1)));
|
||||||
|
|
||||||
|
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
|
||||||
|
assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||||
|
assertThat(account.getUsernameHash()).isEmpty();
|
||||||
|
|
||||||
|
final Map<String, AttributeValue> newUsernameConstraintRecord1 = getUsernameConstraintTableItem(USERNAME_HASH_1);
|
||||||
|
assertThat(newUsernameConstraintRecord1).containsKey(Accounts.ATTR_USERNAME_HASH);
|
||||||
|
assertThat(newUsernameConstraintRecord1).containsKey(Accounts.ATTR_TTL);
|
||||||
|
assertThat(usernameConstraintRecord1.get(Accounts.ATTR_TTL))
|
||||||
|
.isNotEqualTo(newUsernameConstraintRecord1.get(Accounts.ATTR_TTL));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reserveOwnConfirmedUsername() {
|
||||||
|
final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||||
|
createAccount(account);
|
||||||
|
|
||||||
|
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)).join();
|
||||||
|
assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||||
|
assertThat(account.getUsernameHash()).isEmpty();
|
||||||
|
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).containsKey(Accounts.ATTR_TTL);
|
||||||
|
|
||||||
|
|
||||||
|
accounts.confirmUsernameHash(account, USERNAME_HASH_1, ENCRYPTED_USERNAME_1).join();
|
||||||
|
assertThat(account.getReservedUsernameHash()).isEmpty();
|
||||||
|
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||||
|
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).doesNotContainKey(Accounts.ATTR_TTL);
|
||||||
|
|
||||||
|
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
||||||
|
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
|
||||||
|
assertThat(account.getReservedUsernameHash()).isEmpty();
|
||||||
|
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
|
||||||
|
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).containsKey(Accounts.ATTR_USERNAME_HASH);
|
||||||
|
assertThat(getUsernameConstraintTableItem(USERNAME_HASH_1)).doesNotContainKey(Accounts.ATTR_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testUsernameHashAvailable() {
|
void testUsernameHashAvailable() {
|
||||||
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
||||||
|
@ -1119,17 +1166,6 @@ class AccountsTest {
|
||||||
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account2.getUuid());
|
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).join().get().getUuid()).isEqualTo(account2.getUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testRetryReserveUsernameHash() {
|
|
||||||
final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
|
|
||||||
createAccount(account);
|
|
||||||
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(2)).join();
|
|
||||||
|
|
||||||
CompletableFutureTestUtil.assertFailsWithCause(ContestedOptimisticLockException.class,
|
|
||||||
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(2)),
|
|
||||||
"Shouldn't be able to re-reserve same username hash (would extend ttl)");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testReserveConfirmUsernameHashVersionConflict() {
|
void testReserveConfirmUsernameHashVersionConflict() {
|
||||||
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
|
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
|
||||||
|
@ -1300,6 +1336,15 @@ class AccountsTest {
|
||||||
return get.item();
|
return get.item();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, AttributeValue> getUsernameConstraintTableItem(final byte[] usernameHash) {
|
||||||
|
return DYNAMO_DB_EXTENSION.getDynamoDbClient()
|
||||||
|
.getItem(GetItemRequest.builder()
|
||||||
|
.tableName(Tables.USERNAMES.tableName())
|
||||||
|
.key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(usernameHash)))
|
||||||
|
.build())
|
||||||
|
.item();
|
||||||
|
}
|
||||||
|
|
||||||
private void verifyStoredState(String number, UUID uuid, UUID pni, byte[] usernameHash, Account expecting, boolean canonicallyDiscoverable) {
|
private void verifyStoredState(String number, UUID uuid, UUID pni, byte[] usernameHash, Account expecting, boolean canonicallyDiscoverable) {
|
||||||
final DynamoDbClient db = DYNAMO_DB_EXTENSION.getDynamoDbClient();
|
final DynamoDbClient db = DYNAMO_DB_EXTENSION.getDynamoDbClient();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue