Migrate username storage from a relational database to DynamoDB

This commit is contained in:
Jon Chambers
2021-12-01 16:50:18 -05:00
committed by GitHub
parent 0d4a3b1ad4
commit d94e86781f
23 changed files with 664 additions and 764 deletions

View File

@@ -51,6 +51,7 @@ class AccountsManagerChangeNumberIntegrationTest {
private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
private static final String NUMBERS_TABLE_NAME = "numbers_test";
private static final String PNI_ASSIGNMENT_TABLE_NAME = "pni_assignment_test";
private static final String USERNAMES_TABLE_NAME = "usernames_test";
private static final String PNI_TABLE_NAME = "pni_test";
private static final String NEEDS_RECONCILIATION_INDEX_NAME = "needs_reconciliation_test";
private static final String DELETED_ACCOUNTS_LOCK_TABLE_NAME = "deleted_accounts_lock_test";
@@ -155,6 +156,7 @@ class AccountsManagerChangeNumberIntegrationTest {
ACCOUNTS_DYNAMO_EXTENSION.getTableName(),
NUMBERS_TABLE_NAME,
PNI_ASSIGNMENT_TABLE_NAME,
USERNAMES_TABLE_NAME,
SCAN_PAGE_SIZE);
{
@@ -191,7 +193,7 @@ class AccountsManagerChangeNumberIntegrationTest {
mock(DirectoryQueue.class),
mock(Keys.class),
mock(MessagesManager.class),
mock(UsernamesManager.class),
mock(ReservedUsernames.class),
mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class),
secureStorageClient,

View File

@@ -59,6 +59,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
private static final String NUMBERS_TABLE_NAME = "numbers_test";
private static final String PNI_TABLE_NAME = "pni_test";
private static final String USERNAMES_TABLE_NAME = "usernames_test";
private static final int SCAN_PAGE_SIZE = 1;
@@ -122,6 +123,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
dynamoDbExtension.getTableName(),
NUMBERS_TABLE_NAME,
PNI_TABLE_NAME,
USERNAMES_TABLE_NAME,
SCAN_PAGE_SIZE);
{
@@ -148,7 +150,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
mock(DirectoryQueue.class),
mock(Keys.class),
mock(MessagesManager.class),
mock(UsernamesManager.class),
mock(ReservedUsernames.class),
mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class),
mock(SecureStorageClient.class),

View File

@@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.storage;
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -44,25 +45,14 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ContestedOptimisticLockException;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
class AccountsManagerTest {
@@ -73,10 +63,13 @@ class AccountsManagerTest {
private Keys keys;
private MessagesManager messagesManager;
private ProfilesManager profilesManager;
private ReservedUsernames reservedUsernames;
private RedisAdvancedClusterCommands<String, String> commands;
private AccountsManager accountsManager;
private static final UUID RESERVED_FOR_UUID = UUID.randomUUID();
private static final Answer<?> ACCOUNT_UPDATE_ANSWER = (answer) -> {
// it is implicit in the update() contract is that a successful call will
// result in an incremented version
@@ -93,6 +86,7 @@ class AccountsManagerTest {
keys = mock(Keys.class);
messagesManager = mock(MessagesManager.class);
profilesManager = mock(ProfilesManager.class);
reservedUsernames = mock(ReservedUsernames.class);
//noinspection unchecked
commands = mock(RedisAdvancedClusterCommands.class);
@@ -127,6 +121,13 @@ class AccountsManagerTest {
return phoneNumberIdentifiersByE164.computeIfAbsent(number, n -> UUID.randomUUID());
});
@SuppressWarnings("unchecked") final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
mock(DynamicConfigurationManager.class);
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
accountsManager = new AccountsManager(
accounts,
phoneNumberIdentifiers,
@@ -135,7 +136,7 @@ class AccountsManagerTest {
directoryQueue,
keys,
messagesManager,
mock(UsernamesManager.class),
reservedUsernames,
profilesManager,
mock(StoredVerificationCodeManager.class),
storageClient,
@@ -207,6 +208,29 @@ class AccountsManagerTest {
verifyNoInteractions(accounts);
}
@Test
void testGetByUsernameInCache() {
UUID uuid = UUID.randomUUID();
String username = "test";
when(commands.get(eq("AccountMap::" + username))).thenReturn(uuid.toString());
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\", \"username\": \"test\"}");
Optional<Account> account = accountsManager.getByUsername(username);
assertTrue(account.isPresent());
assertEquals(account.get().getNumber(), "+14152222222");
assertEquals(account.get().getProfileName(), "test");
assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier());
assertEquals(Optional.of(username), account.get().getUsername());
verify(commands).get(eq("AccountMap::" + username));
verify(commands).get(eq("Account3::" + uuid));
verifyNoMoreInteractions(commands);
verifyNoInteractions(accounts);
}
@Test
void testGetAccountByNumberNotInCache() {
UUID uuid = UUID.randomUUID();
@@ -280,6 +304,33 @@ class AccountsManagerTest {
verifyNoMoreInteractions(accounts);
}
@Test
void testGetAccountByUsernameNotInCache() {
UUID uuid = UUID.randomUUID();
String username = "test";
Account account = new Account("+14152222222", uuid, UUID.randomUUID(), new HashSet<>(), new byte[16]);
account.setUsername(username);
when(commands.get(eq("AccountMap::" + username))).thenReturn(null);
when(accounts.getByUsername(username)).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.getByUsername(username);
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account);
verify(commands).get(eq("AccountMap::" + username));
verify(commands).setex(eq("AccountMap::" + username), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("AccountMap::" + account.getPhoneNumberIdentifier()), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("Account3::" + uuid), anyLong(), anyString());
verifyNoMoreInteractions(commands);
verify(accounts).getByUsername(username);
verifyNoMoreInteractions(accounts);
}
@Test
void testGetAccountByNumberBrokenCache() {
UUID uuid = UUID.randomUUID();
@@ -353,6 +404,33 @@ class AccountsManagerTest {
verifyNoMoreInteractions(accounts);
}
@Test
void testGetAccountByUsernameBrokenCache() {
UUID uuid = UUID.randomUUID();
String username = "test";
Account account = new Account("+14152222222", uuid, UUID.randomUUID(), new HashSet<>(), new byte[16]);
account.setUsername(username);
when(commands.get(eq("AccountMap::" + username))).thenThrow(new RedisException("OH NO"));
when(accounts.getByUsername(username)).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.getByUsername(username);
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account);
verify(commands).get(eq("AccountMap::" + username));
verify(commands).setex(eq("AccountMap::" + username), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("AccountMap::" + account.getPhoneNumberIdentifier()), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("Account3::" + uuid), anyLong(), anyString());
verifyNoMoreInteractions(commands);
verify(accounts).getByUsername(username);
verifyNoMoreInteractions(accounts);
}
@Test
void testUpdate_optimisticLockingFailure() {
UUID uuid = UUID.randomUUID();
@@ -627,4 +705,54 @@ class AccountsManagerTest {
assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setNumber(targetNumber, UUID.randomUUID())));
}
@Test
void testSetUsername() throws UsernameNotAvailableException {
final Account account = new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new HashSet<>(), new byte[16]);
final String username = "test";
assertDoesNotThrow(() -> accountsManager.setUsername(account, username));
verify(accounts).setUsername(account, username);
}
@Test
void testSetUsernameSameUsername() throws UsernameNotAvailableException {
final Account account = new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new HashSet<>(), new byte[16]);
final String username = "test";
account.setUsername(username);
assertDoesNotThrow(() -> accountsManager.setUsername(account, username));
verify(accounts, never()).setUsername(eq(account), any());
}
@Test
void testSetUsernameNotAvailable() throws UsernameNotAvailableException {
final Account account = new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new HashSet<>(), new byte[16]);
final String username = "test";
doThrow(new UsernameNotAvailableException()).when(accounts).setUsername(account, username);
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.setUsername(account, username));
verify(accounts).setUsername(account, username);
assertTrue(account.getUsername().isEmpty());
}
@Test
void testSetUsernameReserved() {
final String username = "reserved";
when(reservedUsernames.isReserved(eq(username), any())).thenReturn(true);
final Account account = new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new HashSet<>(), new byte[16]);
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.setUsername(account, username));
assertTrue(account.getUsername().isEmpty());
}
@Test
void testSetUsernameViaUpdate() {
final Account account = new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new HashSet<>(), new byte[16]);
assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setUsername("test")));
}
}

View File

@@ -6,6 +6,8 @@
package org.whispersystems.textsecuregcm.storage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
@@ -19,7 +21,6 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -38,8 +39,6 @@ import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.CancellationReason;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
@@ -61,6 +60,7 @@ class AccountsTest {
private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
private static final String NUMBER_CONSTRAINT_TABLE_NAME = "numbers_test";
private static final String PNI_CONSTRAINT_TABLE_NAME = "pni_test";
private static final String USERNAME_CONSTRAINT_TABLE_NAME = "username_test";
private static final int SCAN_PAGE_SIZE = 1;
@@ -108,11 +108,27 @@ class AccountsTest {
dynamoDbExtension.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
CreateTableRequest createUsernamesTableRequest = CreateTableRequest.builder()
.tableName(USERNAME_CONSTRAINT_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_USERNAME)
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(Accounts.ATTR_USERNAME)
.attributeType(ScalarAttributeType.S)
.build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
.build();
dynamoDbExtension.getDynamoDbClient().createTable(createUsernamesTableRequest);
this.accounts = new Accounts(
dynamoDbExtension.getDynamoDbClient(),
dynamoDbExtension.getTableName(),
NUMBER_CONSTRAINT_TABLE_NAME,
PNI_CONSTRAINT_TABLE_NAME,
USERNAME_CONSTRAINT_TABLE_NAME,
SCAN_PAGE_SIZE);
}
@@ -357,7 +373,7 @@ class AccountsTest {
final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class);
accounts = new Accounts(dynamoDbClient,
dynamoDbExtension.getTableName(), NUMBER_CONSTRAINT_TABLE_NAME, PNI_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
dynamoDbExtension.getTableName(), NUMBER_CONSTRAINT_TABLE_NAME, PNI_CONSTRAINT_TABLE_NAME, USERNAME_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
when(dynamoDbClient.updateItem(any(UpdateItemRequest.class)))
.thenThrow(TransactionConflictException.class);
@@ -495,7 +511,7 @@ class AccountsTest {
.thenThrow(RuntimeException.class);
Accounts accounts = new Accounts(client, ACCOUNTS_TABLE_NAME, NUMBER_CONSTRAINT_TABLE_NAME,
PNI_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
PNI_CONSTRAINT_TABLE_NAME, USERNAME_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID());
try {
@@ -646,6 +662,118 @@ class AccountsTest {
assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber, existingPhoneNumberIdentifier));
}
@Test
void testSetUsername() throws UsernameNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
final String username = "test";
assertThat(accounts.getByUsername(username)).isEmpty();
accounts.setUsername(account, username);
{
final Optional<Account> maybeAccount = accounts.getByUsername(username);
assertThat(maybeAccount).hasValueSatisfying(retrievedAccount ->
assertThat(retrievedAccount.getUsername()).hasValueSatisfying(retrievedUsername ->
assertThat(retrievedUsername).isEqualTo(username)));
verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(), maybeAccount.orElseThrow(), account);
}
final String secondUsername = username + "2";
accounts.setUsername(account, secondUsername);
assertThat(accounts.getByUsername(username)).isEmpty();
{
final Optional<Account> maybeAccount = accounts.getByUsername(secondUsername);
assertThat(maybeAccount).isPresent();
verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(),
maybeAccount.get(), account);
}
}
@Test
void testSetUsernameConflict() {
final Account firstAccount = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
final Account secondAccount = generateAccount("+18005559876", UUID.randomUUID(), UUID.randomUUID());
accounts.create(firstAccount);
accounts.create(secondAccount);
final String username = "test";
assertThatNoException().isThrownBy(() -> accounts.setUsername(firstAccount, username));
final Optional<Account> maybeAccount = accounts.getByUsername(username);
assertThat(maybeAccount).isPresent();
verifyStoredState(firstAccount.getNumber(), firstAccount.getUuid(), firstAccount.getPhoneNumberIdentifier(), maybeAccount.get(), firstAccount);
assertThatExceptionOfType(UsernameNotAvailableException.class)
.isThrownBy(() -> accounts.setUsername(secondAccount, username));
assertThat(secondAccount.getUsername()).isEmpty();
}
@Test
void testSetUsernameVersionMismatch() {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
account.setVersion(account.getVersion() + 77);
assertThatExceptionOfType(ContestedOptimisticLockException.class)
.isThrownBy(() -> accounts.setUsername(account, "test"));
assertThat(account.getUsername()).isEmpty();
}
@Test
void testClearUsername() throws UsernameNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
final String username = "test";
accounts.setUsername(account, username);
assertThat(accounts.getByUsername(username)).isPresent();
accounts.clearUsername(account);
assertThat(accounts.getByUsername(username)).isEmpty();
assertThat(accounts.getByAccountIdentifier(account.getUuid()))
.hasValueSatisfying(clearedAccount -> assertThat(clearedAccount.getUsername()).isEmpty());
}
@Test
void testClearUsernameNoUsername() {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
assertThatNoException().isThrownBy(() -> accounts.clearUsername(account));
}
@Test
void testClearUsernameVersionMismatch() throws UsernameNotAvailableException {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
final String username = "test";
accounts.setUsername(account, username);
account.setVersion(account.getVersion() + 12);
assertThatExceptionOfType(ContestedOptimisticLockException.class).isThrownBy(() -> accounts.clearUsername(account));
assertThat(account.getUsername()).hasValueSatisfying(u -> assertThat(u).isEqualTo(username));
}
private Device generateDevice(long id) {
Random random = new Random(System.currentTimeMillis());
SignedPreKey signedPreKey = new SignedPreKey(random.nextInt(), "testPublicKey-" + random.nextInt(), "testSignature-" + random.nextInt());

View File

@@ -90,7 +90,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.storage.UsernameNotAvailableException;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.Hex;
@@ -141,7 +141,6 @@ class AccountControllerTest {
private static RecaptchaClient recaptchaClient = mock(RecaptchaClient.class);
private static GCMSender gcmSender = mock(GCMSender.class);
private static APNSender apnSender = mock(APNSender.class);
private static UsernamesManager usernamesManager = mock(UsernamesManager.class);
private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
@@ -164,7 +163,6 @@ class AccountControllerTest {
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new AccountController(pendingAccountsManager,
accountsManager,
usernamesManager,
abusiveHostRules,
rateLimiters,
smsSender,
@@ -242,8 +240,8 @@ class AccountControllerTest {
return account;
});
when(usernamesManager.put(eq(AuthHelper.VALID_UUID), eq("n00bkiller"))).thenReturn(true);
when(usernamesManager.put(eq(AuthHelper.VALID_UUID), eq("takenusername"))).thenReturn(false);
when(accountsManager.setUsername(AuthHelper.VALID_ACCOUNT, "takenusername"))
.thenThrow(new UsernameNotAvailableException());
{
DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
@@ -293,7 +291,6 @@ class AccountControllerTest {
recaptchaClient,
gcmSender,
apnSender,
usernamesManager,
verifyExperimentEnrollmentManager);
clearInvocations(AuthHelper.DISABLED_DEVICE);
@@ -1622,7 +1619,7 @@ class AccountControllerTest {
.delete();
assertThat(response.getStatus()).isEqualTo(204);
verify(usernamesManager, times(1)).delete(eq(AuthHelper.VALID_UUID));
verify(accountsManager).clearUsername(AuthHelper.VALID_ACCOUNT);
}
@Test

View File

@@ -66,7 +66,6 @@ import org.whispersystems.textsecuregcm.storage.AccountBadge;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
@@ -80,7 +79,6 @@ class ProfileControllerTest {
private static final Clock clock = mock(Clock.class);
private static final AccountsManager accountsManager = mock(AccountsManager.class);
private static final ProfilesManager profilesManager = mock(ProfilesManager.class);
private static final UsernamesManager usernamesManager = mock(UsernamesManager.class);
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
private static final RateLimiter usernameRateLimiter = mock(RateLimiter.class);
@@ -107,7 +105,6 @@ class ProfileControllerTest {
rateLimiters,
accountsManager,
profilesManager,
usernamesManager,
dynamicConfigurationManager,
(acceptableLanguages, accountBadges, isSelf) -> List.of(new Badge("TEST", "other", "Test Badge",
"This badge is in unit tests.", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))
@@ -156,6 +153,7 @@ class ProfileControllerTest {
when(profileAccount.isAnnouncementGroupSupported()).thenReturn(false);
when(profileAccount.isChangeNumberSupported()).thenReturn(false);
when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.empty());
when(profileAccount.getUsername()).thenReturn(Optional.of("n00bkiller"));
Account capabilitiesAccount = mock(Account.class);
@@ -171,8 +169,7 @@ class ProfileControllerTest {
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount));
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID_TWO)).thenReturn(Optional.of(profileAccount));
when(usernamesManager.get(AuthHelper.VALID_UUID_TWO)).thenReturn(Optional.of("n00bkiller"));
when(usernamesManager.get("n00bkiller")).thenReturn(Optional.of(AuthHelper.VALID_UUID_TWO));
when(accountsManager.getByUsername("n00bkiller")).thenReturn(Optional.of(profileAccount));
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount));
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(capabilitiesAccount));
@@ -183,7 +180,6 @@ class ProfileControllerTest {
clearInvocations(rateLimiter);
clearInvocations(accountsManager);
clearInvocations(usernamesManager);
clearInvocations(usernameRateLimiter);
clearInvocations(profilesManager);
}
@@ -209,7 +205,6 @@ class ProfileControllerTest {
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
verify(accountsManager).getByAccountIdentifier(AuthHelper.VALID_UUID_TWO);
verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID);
}
@@ -229,8 +224,7 @@ class ProfileControllerTest {
assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>(
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
verify(accountsManager, times(1)).getByAccountIdentifier(eq(AuthHelper.VALID_UUID_TWO));
verify(usernamesManager, times(1)).get(eq("n00bkiller"));
verify(accountsManager).getByUsername("n00bkiller");
verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID));
}
@@ -265,8 +259,8 @@ class ProfileControllerTest {
assertThat(response.getStatus()).isEqualTo(404);
verify(usernamesManager, times(1)).get(eq("n00bkillerzzzzz"));
verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID));
verify(accountsManager).getByUsername("n00bkillerzzzzz");
verify(usernameRateLimiter).validate(eq(AuthHelper.VALID_UUID));
}
@@ -594,7 +588,6 @@ class ProfileControllerTest {
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
verify(accountsManager, times(1)).getByAccountIdentifier(eq(AuthHelper.VALID_UUID_TWO));
verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"));
verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID);

View File

@@ -1,185 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.storage;
import io.lettuce.core.RedisException;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import org.junit.Test;
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
import org.whispersystems.textsecuregcm.storage.ReservedUsernames;
import org.whispersystems.textsecuregcm.storage.Usernames;
import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
import java.util.Optional;
import java.util.UUID;
import static junit.framework.TestCase.assertSame;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class UsernamesManagerTest {
@Test
public void testGetByUsernameInCache() {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Usernames usernames = mock(Usernames.class);
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
when(commands.get(eq("UsernameByUsername::n00bkiller"))).thenReturn(uuid.toString());
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheCluster);
Optional<UUID> retrieved = usernamesManager.get("n00bkiller");
assertTrue(retrieved.isPresent());
assertEquals(retrieved.get(), uuid);
verify(commands, times(1)).get(eq("UsernameByUsername::n00bkiller"));
verifyNoMoreInteractions(commands);
verifyNoMoreInteractions(usernames);
}
@Test
public void testGetByUuidInCache() {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Usernames usernames = mock(Usernames.class);
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
when(commands.get(eq("UsernameByUuid::" + uuid.toString()))).thenReturn("n00bkiller");
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheCluster);
Optional<String> retrieved = usernamesManager.get(uuid);
assertTrue(retrieved.isPresent());
assertEquals(retrieved.get(), "n00bkiller");
verify(commands, times(1)).get(eq("UsernameByUuid::" + uuid.toString()));
verifyNoMoreInteractions(commands);
verifyNoMoreInteractions(usernames);
}
@Test
public void testGetByUsernameNotInCache() {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Usernames usernames = mock(Usernames.class);
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
when(commands.get(eq("UsernameByUsername::n00bkiller"))).thenReturn(null);
when(usernames.get(eq("n00bkiller"))).thenReturn(Optional.of(uuid));
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheCluster);
Optional<UUID> retrieved = usernamesManager.get("n00bkiller");
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), uuid);
verify(commands, times(1)).get(eq("UsernameByUsername::n00bkiller"));
verify(commands, times(1)).set(eq("UsernameByUsername::n00bkiller"), eq(uuid.toString()));
verify(commands, times(1)).set(eq("UsernameByUuid::" + uuid.toString()), eq("n00bkiller"));
verify(commands, times(1)).get(eq("UsernameByUuid::" + uuid.toString()));
verifyNoMoreInteractions(commands);
verify(usernames, times(1)).get(eq("n00bkiller"));
verifyNoMoreInteractions(usernames);
}
@Test
public void testGetByUuidNotInCache() {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Usernames usernames = mock(Usernames.class);
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
when(commands.get(eq("UsernameByUuid::" + uuid.toString()))).thenReturn(null);
when(usernames.get(eq(uuid))).thenReturn(Optional.of("n00bkiller"));
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheCluster);
Optional<String> retrieved = usernamesManager.get(uuid);
assertTrue(retrieved.isPresent());
assertEquals(retrieved.get(), "n00bkiller");
verify(commands, times(2)).get(eq("UsernameByUuid::" + uuid));
verify(commands, times(1)).set(eq("UsernameByUuid::" + uuid), eq("n00bkiller"));
verify(commands, times(1)).set(eq("UsernameByUsername::n00bkiller"), eq(uuid.toString()));
verifyNoMoreInteractions(commands);
verify(usernames, times(1)).get(eq(uuid));
verifyNoMoreInteractions(usernames);
}
@Test
public void testGetByUsernameBrokenCache() {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Usernames usernames = mock(Usernames.class);
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
when(commands.get(eq("UsernameByUsername::n00bkiller"))).thenThrow(new RedisException("Connection lost!"));
when(usernames.get(eq("n00bkiller"))).thenReturn(Optional.of(uuid));
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheCluster);
Optional<UUID> retrieved = usernamesManager.get("n00bkiller");
assertTrue(retrieved.isPresent());
assertEquals(retrieved.get(), uuid);
verify(commands, times(1)).get(eq("UsernameByUsername::n00bkiller"));
verify(commands, times(1)).set(eq("UsernameByUsername::n00bkiller"), eq(uuid.toString()));
verify(commands, times(1)).set(eq("UsernameByUuid::" + uuid.toString()), eq("n00bkiller"));
verify(commands, times(1)).get(eq("UsernameByUuid::" + uuid.toString()));
verifyNoMoreInteractions(commands);
verify(usernames, times(1)).get(eq("n00bkiller"));
verifyNoMoreInteractions(usernames);
}
@Test
public void testGetAccountByUuidBrokenCache() {
RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
FaultTolerantRedisCluster cacheCluster = RedisClusterHelper.buildMockRedisCluster(commands);
Usernames usernames = mock(Usernames.class);
ReservedUsernames reserved = mock(ReservedUsernames.class);
UUID uuid = UUID.randomUUID();
when(commands.get(eq("UsernameByUuid::" + uuid))).thenThrow(new RedisException("Connection lost!"));
when(usernames.get(eq(uuid))).thenReturn(Optional.of("n00bkiller"));
UsernamesManager usernamesManager = new UsernamesManager(usernames, reserved, cacheCluster);
Optional<String> retrieved = usernamesManager.get(uuid);
assertTrue(retrieved.isPresent());
assertEquals(retrieved.get(), "n00bkiller");
verify(commands, times(2)).get(eq("UsernameByUuid::" + uuid));
verifyNoMoreInteractions(commands);
verify(usernames, times(1)).get(eq(uuid));
verifyNoMoreInteractions(usernames);
}
}

View File

@@ -1,169 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.storage;
import com.opentable.db.postgres.embedded.LiquibasePreparer;
import com.opentable.db.postgres.junit.EmbeddedPostgresRules;
import com.opentable.db.postgres.junit.PreparedDbRule;
import org.jdbi.v3.core.Jdbi;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.storage.FaultTolerantDatabase;
import org.whispersystems.textsecuregcm.storage.Usernames;
import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import java.util.UUID;
import static junit.framework.TestCase.assertTrue;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.Assert.assertFalse;
public class UsernamesTest {
@Rule
public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml"));
private Usernames usernames;
@Before
public void setupAccountsDao() {
FaultTolerantDatabase faultTolerantDatabase = new FaultTolerantDatabase("usernamesTest",
Jdbi.create(db.getTestDatabase()),
new CircuitBreakerConfiguration());
this.usernames = new Usernames(faultTolerantDatabase);
}
@Test
public void testPut() throws SQLException, IOException {
UUID uuid = UUID.randomUUID();
String username = "myusername";
assertTrue(usernames.put(uuid, username));
PreparedStatement statement = db.getTestDatabase().getConnection().prepareStatement("SELECT * FROM usernames WHERE uuid = ?");
verifyStoredState(statement, uuid, username);
}
@Test
public void testPutChange() throws SQLException, IOException {
UUID uuid = UUID.randomUUID();
String firstUsername = "myfirstusername";
String secondUsername = "mysecondusername";
assertTrue(usernames.put(uuid, firstUsername));
PreparedStatement statement = db.getTestDatabase().getConnection().prepareStatement("SELECT * FROM usernames WHERE uuid = ?");
verifyStoredState(statement, uuid, firstUsername);
assertTrue(usernames.put(uuid, secondUsername));
verifyStoredState(statement, uuid, secondUsername);
}
@Test
public void testPutConflict() throws SQLException {
UUID firstUuid = UUID.randomUUID();
UUID secondUuid = UUID.randomUUID();
String username = "myfirstusername";
assertTrue(usernames.put(firstUuid, username));
assertFalse(usernames.put(secondUuid, username));
PreparedStatement statement = db.getTestDatabase().getConnection().prepareStatement("SELECT * FROM usernames WHERE username = ?");
statement.setString(1, username);
ResultSet resultSet = statement.executeQuery();
assertTrue(resultSet.next());
assertThat(resultSet.getString("uuid")).isEqualTo(firstUuid.toString());
assertThat(resultSet.next()).isFalse();
}
@Test
public void testGetByUuid() {
UUID uuid = UUID.randomUUID();
String username = "myusername";
assertTrue(usernames.put(uuid, username));
Optional<String> retrieved = usernames.get(uuid);
assertTrue(retrieved.isPresent());
assertThat(retrieved.get()).isEqualTo(username);
}
@Test
public void testGetByUuidMissing() {
Optional<String> retrieved = usernames.get(UUID.randomUUID());
assertFalse(retrieved.isPresent());
}
@Test
public void testGetByUsername() {
UUID uuid = UUID.randomUUID();
String username = "myusername";
assertTrue(usernames.put(uuid, username));
Optional<UUID> retrieved = usernames.get(username);
assertTrue(retrieved.isPresent());
assertThat(retrieved.get()).isEqualTo(uuid);
}
@Test
public void testGetByUsernameMissing() {
Optional<UUID> retrieved = usernames.get("myusername");
assertFalse(retrieved.isPresent());
}
@Test
public void testDelete() {
UUID uuid = UUID.randomUUID();
String username = "myusername";
assertTrue(usernames.put(uuid, username));
Optional<UUID> retrieved = usernames.get(username);
assertTrue(retrieved.isPresent());
assertThat(retrieved.get()).isEqualTo(uuid);
usernames.delete(uuid);
assertThat(usernames.get(uuid).isPresent()).isFalse();
}
private void verifyStoredState(PreparedStatement statement, UUID uuid, String expectedUsername)
throws SQLException, IOException
{
statement.setObject(1, uuid);
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
String data = resultSet.getString("username");
assertThat(data).isNotEmpty();
assertThat(data).isEqualTo(expectedUsername);
} else {
throw new AssertionError("No data");
}
assertThat(resultSet.next()).isFalse();
}
}

View File

@@ -103,6 +103,10 @@ public class AccountsHelper {
when(updatedAccount.getNumber()).thenAnswer(stubbing);
break;
}
case "getUsername": {
when(updatedAccount.getUsername()).thenAnswer(stubbing);
break;
}
case "getDevices": {
when(updatedAccount.getDevices())
.thenAnswer(stubbing);