More Accounts cleanup

* Remove `AccountStore`
* Clean up `AccountsDynamoDb#delete`
* Rename `AccountsDynamoDb` → `Accounts`
* Remove unused configuration
* Move Accounts scan page size to static configuration
* Remove disabled tests and related methods
This commit is contained in:
Chris Eager 2021-09-21 15:25:16 -07:00 committed by GitHub
parent 75661fa800
commit 6a71d369e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 232 additions and 513 deletions

View File

@ -109,11 +109,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private AccountDatabaseCrawlerConfiguration accountDatabaseCrawler; private AccountDatabaseCrawlerConfiguration accountDatabaseCrawler;
@NotNull
@Valid
@JsonProperty
private AccountDatabaseCrawlerConfiguration dynamoDbMigrationCrawler;
@NotNull @NotNull
@Valid @Valid
@JsonProperty @JsonProperty
@ -149,21 +144,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private AccountsDynamoDbConfiguration accountsDynamoDb; private AccountsDynamoDbConfiguration accountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration migrationDeletedAccountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration migrationMismatchedAccountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration migrationRetryAccountsDynamoDb;
@Valid @Valid
@NotNull @NotNull
@JsonProperty @JsonProperty
@ -376,10 +356,6 @@ public class WhisperServerConfiguration extends Configuration {
return accountDatabaseCrawler; return accountDatabaseCrawler;
} }
public AccountDatabaseCrawlerConfiguration getDynamoDbMigrationCrawlerConfiguration() {
return dynamoDbMigrationCrawler;
}
public MessageCacheConfiguration getMessageCacheConfiguration() { public MessageCacheConfiguration getMessageCacheConfiguration() {
return messageCache; return messageCache;
} }
@ -408,18 +384,6 @@ public class WhisperServerConfiguration extends Configuration {
return accountsDynamoDb; return accountsDynamoDb;
} }
public DynamoDbConfiguration getMigrationDeletedAccountsDynamoDbConfiguration() {
return migrationDeletedAccountsDynamoDb;
}
public DynamoDbConfiguration getMigrationMismatchedAccountsDynamoDbConfiguration() {
return migrationMismatchedAccountsDynamoDb;
}
public DynamoDbConfiguration getMigrationRetryAccountsDynamoDbConfiguration() {
return migrationRetryAccountsDynamoDb;
}
public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() { public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() {
return deletedAccountsDynamoDb; return deletedAccountsDynamoDb;
} }

View File

@ -154,7 +154,7 @@ import org.whispersystems.textsecuregcm.storage.AccountCleaner;
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawler; import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawler;
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerCache; import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerCache;
import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerListener; import org.whispersystems.textsecuregcm.storage.AccountDatabaseCrawlerListener;
import org.whispersystems.textsecuregcm.storage.AccountsDynamoDb; import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ContactDiscoveryWriter; import org.whispersystems.textsecuregcm.storage.ContactDiscoveryWriter;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts; import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
@ -337,10 +337,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getDeletedAccountsDynamoDbConfiguration().getTableName(), config.getDeletedAccountsDynamoDbConfiguration().getTableName(),
config.getDeletedAccountsDynamoDbConfiguration().getNeedsReconciliationIndexName()); config.getDeletedAccountsDynamoDbConfiguration().getNeedsReconciliationIndexName());
AccountsDynamoDb accountsDynamoDb = new AccountsDynamoDb(accountsDynamoDbClient, Accounts accounts = new Accounts(accountsDynamoDbClient,
config.getAccountsDynamoDbConfiguration().getTableName(), config.getAccountsDynamoDbConfiguration().getTableName(),
config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName() config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
); config.getAccountsDynamoDbConfiguration().getScanPageSize());
Usernames usernames = new Usernames(accountDatabase); Usernames usernames = new Usernames(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase); ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
Profiles profiles = new Profiles(accountDatabase); Profiles profiles = new Profiles(accountDatabase);
@ -424,10 +424,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager); MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, pushLatencyManager, reportMessageManager);
DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts, DeletedAccountsManager deletedAccountsManager = new DeletedAccountsManager(deletedAccounts,
deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName()); deletedAccountsLockDynamoDbClient, config.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
AccountsManager accountsManager = new AccountsManager(accountsDynamoDb, cacheCluster, AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
profilesManager, pendingAccountsManager, secureStorageClient, secureBackupClient, pendingAccountsManager, secureStorageClient, secureBackupClient);
dynamicConfigurationManager);
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs); RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager); DeadLetterHandler deadLetterHandler = new DeadLetterHandler(accountsManager, messagesManager);
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.of(deadLetterHandler)); DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.of(deadLetterHandler));

View File

@ -1,5 +1,6 @@
package org.whispersystems.textsecuregcm.configuration; package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration { public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
@ -7,7 +8,16 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
@NotNull @NotNull
private String phoneNumberTableName; private String phoneNumberTableName;
private int scanPageSize = 100;
@JsonProperty
public String getPhoneNumberTableName() { public String getPhoneNumberTableName() {
return phoneNumberTableName; return phoneNumberTableName;
} }
@JsonProperty
public int getScanPageSize() {
return scanPageSize;
}
} }

View File

@ -1,15 +0,0 @@
package org.whispersystems.textsecuregcm.configuration.dynamic;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DynamicAccountsDynamoDbMigrationConfiguration {
@JsonProperty
int dynamoCrawlerScanPageSize = 10;
// TODO move out of "migration" configuration
public int getDynamoCrawlerScanPageSize() {
return dynamoCrawlerScanPageSize;
}
}

View File

@ -44,9 +44,6 @@ public class DynamicConfiguration {
@JsonProperty @JsonProperty
private DynamicSignupCaptchaConfiguration signupCaptcha = new DynamicSignupCaptchaConfiguration(); private DynamicSignupCaptchaConfiguration signupCaptcha = new DynamicSignupCaptchaConfiguration();
@JsonProperty
private DynamicAccountsDynamoDbMigrationConfiguration accountsDynamoDbMigration = new DynamicAccountsDynamoDbMigrationConfiguration();
@JsonProperty @JsonProperty
@Valid @Valid
private DynamicRateLimitChallengeConfiguration rateLimitChallenge = new DynamicRateLimitChallengeConfiguration(); private DynamicRateLimitChallengeConfiguration rateLimitChallenge = new DynamicRateLimitChallengeConfiguration();
@ -94,10 +91,6 @@ public class DynamicConfiguration {
return signupCaptcha; return signupCaptcha;
} }
public DynamicAccountsDynamoDbMigrationConfiguration getAccountsDynamoDbMigrationConfiguration() {
return accountsDynamoDbMigration;
}
public DynamicRateLimitChallengeConfiguration getRateLimitChallengeConfiguration() { public DynamicRateLimitChallengeConfiguration getRateLimitChallengeConfiguration() {
return rateLimitChallenge; return rateLimitChallenge;
} }

View File

@ -106,7 +106,7 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
final long endTimeMs = System.currentTimeMillis(); final long endTimeMs = System.currentTimeMillis();
final long sleepIntervalMs = chunkIntervalMs - (endTimeMs - startTimeMs); final long sleepIntervalMs = chunkIntervalMs - (endTimeMs - startTimeMs);
if (sleepIntervalMs > 0) { if (sleepIntervalMs > 0) {
logger.info("Sleeping {}ms", sleepIntervalMs); logger.debug("Sleeping {}ms", sleepIntervalMs);
sleepWhileRunning(sleepIntervalMs); sleepWhileRunning(sleepIntervalMs);
} }
} finally { } finally {
@ -135,7 +135,7 @@ public class AccountDatabaseCrawler implements Managed, Runnable {
cacheLastUuid(Optional.empty()); cacheLastUuid(Optional.empty());
cache.setAccelerated(false); cache.setAccelerated(false);
} else { } else {
logger.info("Processing chunk"); logger.debug("Processing chunk");
try { try {
for (AccountDatabaseCrawlerListener listener : listeners) { for (AccountDatabaseCrawlerListener listener : listeners) {
listener.timeAndProcessCrawlChunk(fromUuid, chunkAccounts.getAccounts()); listener.timeAndProcessCrawlChunk(fromUuid, chunkAccounts.getAccounts());

View File

@ -1,17 +0,0 @@
package org.whispersystems.textsecuregcm.storage;
import java.util.Optional;
import java.util.UUID;
public interface AccountStore {
boolean create(Account account);
void update(Account account) throws ContestedOptimisticLockException;
Optional<Account> get(String number);
Optional<Account> get(UUID uuid);
void delete(final UUID uuid);
}

View File

@ -38,7 +38,7 @@ import software.amazon.awssdk.services.dynamodb.model.TransactionConflictExcepti
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountStore { public class Accounts extends AbstractDynamoDbStore {
// uuid, primary key // uuid, primary key
static final String KEY_ACCOUNT_UUID = "U"; static final String KEY_ACCOUNT_UUID = "U";
@ -56,25 +56,28 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
private final String phoneNumbersTableName; private final String phoneNumbersTableName;
private final String accountsTableName; private final String accountsTableName;
private static final Timer CREATE_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "create")); private final int scanPageSize;
private static final Timer UPDATE_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "update"));
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getByNumber")); private static final Timer CREATE_TIMER = Metrics.timer(name(Accounts.class, "create"));
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getByUuid")); private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getAllFrom")); private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "getAllFromOffset")); private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
private static final Timer DELETE_TIMER = Metrics.timer(name(AccountsDynamoDb.class, "delete")); private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
public AccountsDynamoDb(DynamoDbClient client, String accountsTableName, String phoneNumbersTableName) { public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumbersTableName,
final int scanPageSize) {
super(client); super(client);
this.client = client; this.client = client;
this.phoneNumbersTableName = phoneNumbersTableName; this.phoneNumbersTableName = phoneNumbersTableName;
this.accountsTableName = accountsTableName; this.accountsTableName = accountsTableName;
this.scanPageSize = scanPageSize;
} }
@Override
public boolean create(Account account) { public boolean create(Account account) {
return CREATE_TIMER.record(() -> { return CREATE_TIMER.record(() -> {
@ -115,7 +118,7 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
} }
if ("TransactionConflict".equals(accountCancellationReason.code())) { if ("TransactionConflict".equals(accountCancellationReason.code())) {
// this should only happen during concurrent update()s for an account migration // this should only happen if two clients manage to make concurrent create() calls
throw new ContestedOptimisticLockException(); throw new ContestedOptimisticLockException();
} }
@ -164,7 +167,6 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
.build(); .build();
} }
@Override
public void update(Account account) throws ContestedOptimisticLockException { public void update(Account account) throws ContestedOptimisticLockException {
UPDATE_TIMER.record(() -> { UPDATE_TIMER.record(() -> {
UpdateItemRequest updateItemRequest; UpdateItemRequest updateItemRequest;
@ -207,8 +209,6 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
}); });
} }
@Override
public Optional<Account> get(String number) { public Optional<Account> get(String number) {
return GET_BY_NUMBER_TIMER.record(() -> { return GET_BY_NUMBER_TIMER.record(() -> {
@ -220,7 +220,7 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
return Optional.ofNullable(response.item()) return Optional.ofNullable(response.item())
.map(item -> item.get(KEY_ACCOUNT_UUID)) .map(item -> item.get(KEY_ACCOUNT_UUID))
.map(uuid -> accountByUuid(uuid)) .map(uuid -> accountByUuid(uuid))
.map(AccountsDynamoDb::fromItem); .map(Accounts::fromItem);
}); });
} }
@ -233,29 +233,52 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
return r.item().isEmpty() ? null : r.item(); return r.item().isEmpty() ? null : r.item();
} }
@Override
public Optional<Account> get(UUID uuid) { public Optional<Account> get(UUID uuid) {
return GET_BY_UUID_TIMER.record(() -> return GET_BY_UUID_TIMER.record(() ->
Optional.ofNullable(accountByUuid(AttributeValues.fromUUID(uuid))) Optional.ofNullable(accountByUuid(AttributeValues.fromUUID(uuid)))
.map(AccountsDynamoDb::fromItem)); .map(Accounts::fromItem));
} }
@Override
public void delete(UUID uuid) { public void delete(UUID uuid) {
DELETE_TIMER.record(() -> delete(uuid, true)); DELETE_TIMER.record(() -> {
Optional<Account> maybeAccount = get(uuid);
maybeAccount.ifPresent(account -> {
TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumbersTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
.build())
.build();
TransactWriteItem accountDelete = TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
.build())
.build();
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(phoneNumberDelete, accountDelete).build();
client.transactWriteItems(request);
});
});
} }
public AccountCrawlChunk getAllFrom(final UUID from, final int maxCount, final int pageSize) { public AccountCrawlChunk getAllFrom(final UUID from, final int maxCount) {
final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder() final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder()
.limit(pageSize) .limit(scanPageSize)
.exclusiveStartKey(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(from))); .exclusiveStartKey(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(from)));
return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_OFFSET_TIMER); return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_OFFSET_TIMER);
} }
public AccountCrawlChunk getAllFromStart(final int maxCount, final int pageSize) { public AccountCrawlChunk getAllFromStart(final int maxCount) {
final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder() final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder()
.limit(pageSize); .limit(scanPageSize);
return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_START_TIMER); return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_START_TIMER);
} }
@ -266,39 +289,12 @@ public class AccountsDynamoDb extends AbstractDynamoDbStore implements AccountSt
final List<Account> accounts = timer.record(() -> scan(scanRequestBuilder.build(), maxCount) final List<Account> accounts = timer.record(() -> scan(scanRequestBuilder.build(), maxCount)
.stream() .stream()
.map(AccountsDynamoDb::fromItem) .map(Accounts::fromItem)
.collect(Collectors.toList())); .collect(Collectors.toList()));
return new AccountCrawlChunk(accounts, accounts.size() > 0 ? accounts.get(accounts.size() - 1).getUuid() : null); return new AccountCrawlChunk(accounts, accounts.size() > 0 ? accounts.get(accounts.size() - 1).getUuid() : null);
} }
private void delete(UUID uuid, boolean saveInDeletedAccountsTable) {
Optional<Account> maybeAccount = get(uuid);
maybeAccount.ifPresent(account -> {
TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumbersTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
.build())
.build();
TransactWriteItem accountDelete = TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
.build())
.build();
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(phoneNumberDelete, accountDelete).build();
client.transactWriteItems(request);
});
}
private static String extractCancellationReasonCodes(final TransactionCanceledException exception) { private static String extractCancellationReasonCodes(final TransactionCanceledException exception) {
return exception.cancellationReasons().stream() return exception.cancellationReasons().stream()
.map(CancellationReason::code) .map(CancellationReason::code)

View File

@ -18,15 +18,12 @@ import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Tags;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
@ -65,7 +62,7 @@ public class AccountsManager {
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class); private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
private final AccountsDynamoDb accountsDynamoDb; private final Accounts accounts;
private final FaultTolerantRedisCluster cacheCluster; private final FaultTolerantRedisCluster cacheCluster;
private final DeletedAccountsManager deletedAccountsManager; private final DeletedAccountsManager deletedAccountsManager;
private final DirectoryQueue directoryQueue; private final DirectoryQueue directoryQueue;
@ -78,8 +75,6 @@ public class AccountsManager {
private final SecureBackupClient secureBackupClient; private final SecureBackupClient secureBackupClient;
private final ObjectMapper mapper; private final ObjectMapper mapper;
private final DynamicConfigurationManager dynamicConfigurationManager;
public enum DeletionReason { public enum DeletionReason {
ADMIN_DELETED("admin"), ADMIN_DELETED("admin"),
EXPIRED ("expired"), EXPIRED ("expired"),
@ -92,7 +87,7 @@ public class AccountsManager {
} }
} }
public AccountsManager(AccountsDynamoDb accountsDynamoDb, FaultTolerantRedisCluster cacheCluster, public AccountsManager(Accounts accounts, FaultTolerantRedisCluster cacheCluster,
final DeletedAccountsManager deletedAccountsManager, final DeletedAccountsManager deletedAccountsManager,
final DirectoryQueue directoryQueue, final DirectoryQueue directoryQueue,
final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager, final KeysDynamoDb keysDynamoDb, final MessagesManager messagesManager,
@ -100,22 +95,20 @@ public class AccountsManager {
final ProfilesManager profilesManager, final ProfilesManager profilesManager,
final StoredVerificationCodeManager pendingAccounts, final StoredVerificationCodeManager pendingAccounts,
final SecureStorageClient secureStorageClient, final SecureStorageClient secureStorageClient,
final SecureBackupClient secureBackupClient, final SecureBackupClient secureBackupClient) {
final DynamicConfigurationManager dynamicConfigurationManager) { this.accounts = accounts;
this.accountsDynamoDb = accountsDynamoDb; this.cacheCluster = cacheCluster;
this.cacheCluster = cacheCluster;
this.deletedAccountsManager = deletedAccountsManager; this.deletedAccountsManager = deletedAccountsManager;
this.directoryQueue = directoryQueue; this.directoryQueue = directoryQueue;
this.keysDynamoDb = keysDynamoDb; this.keysDynamoDb = keysDynamoDb;
this.messagesManager = messagesManager; this.messagesManager = messagesManager;
this.usernamesManager = usernamesManager; this.usernamesManager = usernamesManager;
this.profilesManager = profilesManager; this.profilesManager = profilesManager;
this.pendingAccounts = pendingAccounts; this.pendingAccounts = pendingAccounts;
this.secureStorageClient = secureStorageClient; this.secureStorageClient = secureStorageClient;
this.secureBackupClient = secureBackupClient; this.secureBackupClient = secureBackupClient;
this.mapper = SystemMapper.getMapper(); this.mapper = SystemMapper.getMapper();
this.dynamicConfigurationManager = dynamicConfigurationManager;
} }
public Account create(final String number, public Account create(final String number,
@ -336,15 +329,11 @@ public class AccountsManager {
} }
public AccountCrawlChunk getAllFromDynamo(int length) { public AccountCrawlChunk getAllFromDynamo(int length) {
final int maxPageSize = dynamicConfigurationManager.getConfiguration().getAccountsDynamoDbMigrationConfiguration() return accounts.getAllFromStart(length);
.getDynamoCrawlerScanPageSize();
return accountsDynamoDb.getAllFromStart(length, maxPageSize);
} }
public AccountCrawlChunk getAllFromDynamo(UUID uuid, int length) { public AccountCrawlChunk getAllFromDynamo(UUID uuid, int length) {
final int maxPageSize = dynamicConfigurationManager.getConfiguration().getAccountsDynamoDbMigrationConfiguration() return accounts.getAllFrom(uuid, length);
.getDynamoCrawlerScanPageSize();
return accountsDynamoDb.getAllFrom(uuid, length, maxPageSize);
} }
public void delete(final Account account, final DeletionReason deletionReason) throws InterruptedException { public void delete(final Account account, final DeletionReason deletionReason) throws InterruptedException {
@ -445,38 +434,23 @@ public class AccountsManager {
} }
private Optional<Account> dynamoGet(String number) { private Optional<Account> dynamoGet(String number) {
return accountsDynamoDb.get(number); return accounts.get(number);
} }
private Optional<Account> dynamoGet(UUID uuid) { private Optional<Account> dynamoGet(UUID uuid) {
return accountsDynamoDb.get(uuid); return accounts.get(uuid);
} }
private boolean dynamoCreate(Account account) { private boolean dynamoCreate(Account account) {
return accountsDynamoDb.create(account); return accounts.create(account);
} }
private void dynamoUpdate(Account account) { private void dynamoUpdate(Account account) {
accountsDynamoDb.update(account); accounts.update(account);
} }
private void dynamoDelete(final Account account) { private void dynamoDelete(final Account account) {
accountsDynamoDb.delete(account.getUuid()); accounts.delete(account.getUuid());
} }
// TODO delete
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Deprecated
public Optional<String> compareAccounts(final Optional<Account> maybePrimaryAccount,
final Optional<Account> maybeSecondaryAccount) {
return Optional.empty();
}
private String getAbbreviatedCallChain(final StackTraceElement[] stackTrace) {
return Arrays.stream(stackTrace)
.filter(stackTraceElement -> stackTraceElement.getClassName().contains("org.whispersystems"))
.filter(stackTraceElement -> !(stackTraceElement.getClassName().endsWith("AccountsManager") && stackTraceElement.getMethodName().contains("compare")))
.map(stackTraceElement -> StringUtils.substringAfterLast(stackTraceElement.getClassName(), ".") + ":" + stackTraceElement.getMethodName())
.collect(Collectors.joining(" -> "));
}
} }

View File

@ -33,7 +33,7 @@ import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue; import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsDynamoDb; import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.AccountsManager.DeletionReason; import org.whispersystems.textsecuregcm.storage.AccountsManager.DeletionReason;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts; import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
@ -150,9 +150,10 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
VerificationCodeStore pendingAccounts = new VerificationCodeStore(pendingAccountsDynamoDbClient, VerificationCodeStore pendingAccounts = new VerificationCodeStore(pendingAccountsDynamoDbClient,
configuration.getPendingAccountsDynamoDbConfiguration().getTableName()); configuration.getPendingAccountsDynamoDbConfiguration().getTableName());
AccountsDynamoDb accountsDynamoDb = new AccountsDynamoDb(accountsDynamoDbClient, Accounts accounts = new Accounts(accountsDynamoDbClient,
configuration.getAccountsDynamoDbConfiguration().getTableName(), configuration.getAccountsDynamoDbConfiguration().getTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName()); configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
Usernames usernames = new Usernames(accountDatabase); Usernames usernames = new Usernames(accountDatabase);
Profiles profiles = new Profiles(accountDatabase); Profiles profiles = new Profiles(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase); ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
@ -188,10 +189,9 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
deletedAccountsLockDynamoDbClient, deletedAccountsLockDynamoDbClient,
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName()); configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts); StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
AccountsManager accountsManager = new AccountsManager(accountsDynamoDb, cacheCluster, AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
profilesManager, pendingAccountsManager, secureStorageClient, secureBackupClient, pendingAccountsManager, secureStorageClient, secureBackupClient);
dynamicConfigurationManager);
for (String user : users) { for (String user : users) {
Optional<Account> account = accountsManager.get(user); Optional<Account> account = accountsManager.get(user);

View File

@ -32,7 +32,7 @@ import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue; import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsDynamoDb; import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DeletedAccounts; import org.whispersystems.textsecuregcm.storage.DeletedAccounts;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager; import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
@ -153,10 +153,10 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
VerificationCodeStore pendingAccounts = new VerificationCodeStore(pendingAccountsDynamoDbClient, VerificationCodeStore pendingAccounts = new VerificationCodeStore(pendingAccountsDynamoDbClient,
configuration.getPendingAccountsDynamoDbConfiguration().getTableName()); configuration.getPendingAccountsDynamoDbConfiguration().getTableName());
AccountsDynamoDb accountsDynamoDb = new AccountsDynamoDb(accountsDynamoDbClient, Accounts accounts = new Accounts(accountsDynamoDbClient,
configuration.getAccountsDynamoDbConfiguration().getTableName(), configuration.getAccountsDynamoDbConfiguration().getTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName() configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
); configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
Usernames usernames = new Usernames(accountDatabase); Usernames usernames = new Usernames(accountDatabase);
Profiles profiles = new Profiles(accountDatabase); Profiles profiles = new Profiles(accountDatabase);
ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase); ReservedUsernames reservedUsernames = new ReservedUsernames(accountDatabase);
@ -192,11 +192,9 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
deletedAccountsLockDynamoDbClient, deletedAccountsLockDynamoDbClient,
configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName()); configuration.getDeletedAccountsLockDynamoDbConfiguration().getTableName());
StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts); StoredVerificationCodeManager pendingAccountsManager = new StoredVerificationCodeManager(pendingAccounts);
AccountsManager accountsManager = new AccountsManager(accountsDynamoDb, cacheCluster, AccountsManager accountsManager = new AccountsManager(accounts, cacheCluster,
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
profilesManager, pendingAccountsManager, secureStorageClient, secureBackupClient);
pendingAccountsManager, secureStorageClient, secureBackupClient,
dynamicConfigurationManager);
Optional<Account> maybeAccount; Optional<Account> maybeAccount;

View File

@ -313,29 +313,6 @@ class DynamicConfigurationTest {
} }
} }
@Test
void testParseAccountsDynamoDbMigrationConfiguration() throws JsonProcessingException {
{
final String emptyConfigYaml = "test: true";
final DynamicConfiguration emptyConfig =
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml).orElseThrow();
assertEquals(10, emptyConfig.getAccountsDynamoDbMigrationConfiguration().getDynamoCrawlerScanPageSize());
}
{
final String accountsDynamoDbMigrationConfig =
"accountsDynamoDbMigration:\n"
+ " dynamoCrawlerScanPageSize: 5000";
final DynamicAccountsDynamoDbMigrationConfiguration config =
DynamicConfigurationManager.parseConfiguration(accountsDynamoDbMigrationConfig).orElseThrow()
.getAccountsDynamoDbMigrationConfiguration();
assertEquals(5000, config.getDynamoCrawlerScanPageSize());
}
}
@Test @Test
void testParseLimits() throws JsonProcessingException { void testParseLimits() throws JsonProcessingException {
{ {

View File

@ -33,8 +33,6 @@ public class AccountDatabaseCrawlerIntegrationTest extends AbstractRedisClusterT
private AccountsManager accountsManager; private AccountsManager accountsManager;
private AccountDatabaseCrawlerListener listener; private AccountDatabaseCrawlerListener listener;
private DynamicConfigurationManager dynamicConfigurationManager;
private AccountDatabaseCrawler accountDatabaseCrawler; private AccountDatabaseCrawler accountDatabaseCrawler;
private static final int CHUNK_SIZE = 1; private static final int CHUNK_SIZE = 1;
@ -50,8 +48,6 @@ public class AccountDatabaseCrawlerIntegrationTest extends AbstractRedisClusterT
accountsManager = mock(AccountsManager.class); accountsManager = mock(AccountsManager.class);
listener = mock(AccountDatabaseCrawlerListener.class); listener = mock(AccountDatabaseCrawlerListener.class);
dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
when(firstAccount.getUuid()).thenReturn(FIRST_UUID); when(firstAccount.getUuid()).thenReturn(FIRST_UUID);
when(secondAccount.getUuid()).thenReturn(SECOND_UUID); when(secondAccount.getUuid()).thenReturn(SECOND_UUID);

View File

@ -15,11 +15,7 @@ import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.opentable.db.postgres.embedded.LiquibasePreparer;
import com.opentable.db.postgres.junit5.EmbeddedPostgresExtension;
import com.opentable.db.postgres.junit5.PreparedDbExtension;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.time.Instant;
@ -38,7 +34,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.SignedPreKey; import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient; import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
@ -55,23 +50,22 @@ import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
class AccountsManagerConcurrentModificationIntegrationTest { class AccountsManagerConcurrentModificationIntegrationTest {
@RegisterExtension
static PreparedDbExtension db = EmbeddedPostgresExtension.preparedDatabase(LiquibasePreparer.forClasspathLocation("accountsdb.xml"));
private static final String ACCOUNTS_TABLE_NAME = "accounts_test"; private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
private static final String NUMBERS_TABLE_NAME = "numbers_test"; private static final String NUMBERS_TABLE_NAME = "numbers_test";
private static final int SCAN_PAGE_SIZE = 1;
@RegisterExtension @RegisterExtension
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder() static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
.tableName(ACCOUNTS_TABLE_NAME) .tableName(ACCOUNTS_TABLE_NAME)
.hashKey(AccountsDynamoDb.KEY_ACCOUNT_UUID) .hashKey(Accounts.KEY_ACCOUNT_UUID)
.attributeDefinition(AttributeDefinition.builder() .attributeDefinition(AttributeDefinition.builder()
.attributeName(AccountsDynamoDb.KEY_ACCOUNT_UUID) .attributeName(Accounts.KEY_ACCOUNT_UUID)
.attributeType(ScalarAttributeType.B) .attributeType(ScalarAttributeType.B)
.build()) .build())
.build(); .build();
private AccountsDynamoDb accountsDynamoDb; private Accounts accounts;
private AccountsManager accountsManager; private AccountsManager accountsManager;
@ -86,11 +80,11 @@ class AccountsManagerConcurrentModificationIntegrationTest {
CreateTableRequest createNumbersTableRequest = CreateTableRequest.builder() CreateTableRequest createNumbersTableRequest = CreateTableRequest.builder()
.tableName(NUMBERS_TABLE_NAME) .tableName(NUMBERS_TABLE_NAME)
.keySchema(KeySchemaElement.builder() .keySchema(KeySchemaElement.builder()
.attributeName(AccountsDynamoDb.ATTR_ACCOUNT_E164) .attributeName(Accounts.ATTR_ACCOUNT_E164)
.keyType(KeyType.HASH) .keyType(KeyType.HASH)
.build()) .build())
.attributeDefinitions(AttributeDefinition.builder() .attributeDefinitions(AttributeDefinition.builder()
.attributeName(AccountsDynamoDb.ATTR_ACCOUNT_E164) .attributeName(Accounts.ATTR_ACCOUNT_E164)
.attributeType(ScalarAttributeType.S) .attributeType(ScalarAttributeType.S)
.build()) .build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT) .provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
@ -99,18 +93,14 @@ class AccountsManagerConcurrentModificationIntegrationTest {
dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest); dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest);
} }
accountsDynamoDb = new AccountsDynamoDb( accounts = new Accounts(
dynamoDbExtension.getDynamoDbClient(), dynamoDbExtension.getDynamoDbClient(),
dynamoDbExtension.getTableName(), dynamoDbExtension.getTableName(),
NUMBERS_TABLE_NAME NUMBERS_TABLE_NAME,
); SCAN_PAGE_SIZE);
{ {
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); //noinspection unchecked
DynamicConfiguration dynamicConfiguration = new DynamicConfiguration();
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
commands = mock(RedisAdvancedClusterCommands.class); commands = mock(RedisAdvancedClusterCommands.class);
final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class); final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
@ -122,7 +112,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
}).when(deletedAccountsManager).lockAndTake(anyString(), any()); }).when(deletedAccountsManager).lockAndTake(anyString(), any());
accountsManager = new AccountsManager( accountsManager = new AccountsManager(
accountsDynamoDb, accounts,
RedisClusterHelper.buildMockRedisCluster(commands), RedisClusterHelper.buildMockRedisCluster(commands),
deletedAccountsManager, deletedAccountsManager,
mock(DirectoryQueue.class), mock(DirectoryQueue.class),
@ -132,8 +122,8 @@ class AccountsManagerConcurrentModificationIntegrationTest {
mock(ProfilesManager.class), mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class), mock(StoredVerificationCodeManager.class),
mock(SecureStorageClient.class), mock(SecureStorageClient.class),
mock(SecureBackupClient.class), mock(SecureBackupClient.class)
dynamicConfigurationManager); );
} }
} }
@ -186,12 +176,12 @@ class AccountsManagerConcurrentModificationIntegrationTest {
modifyAccount(uuid, account -> account.setUnidentifiedAccessKey(unidentifiedAccessKey)), modifyAccount(uuid, account -> account.setUnidentifiedAccessKey(unidentifiedAccessKey)),
modifyAccount(uuid, account -> account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt())), modifyAccount(uuid, account -> account.setRegistrationLock(credentials.getHashedAuthenticationToken(), credentials.getSalt())),
modifyAccount(uuid, account -> account.setUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedAccess)), modifyAccount(uuid, account -> account.setUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedAccess)),
modifyDevice(uuid, Device.MASTER_ID, device-> device.setLastSeen(lastSeen)), modifyDevice(uuid, Device.MASTER_ID, device -> device.setLastSeen(lastSeen)),
modifyDevice(uuid, Device.MASTER_ID, device-> device.setName("deviceName")) modifyDevice(uuid, Device.MASTER_ID, device -> device.setName("deviceName"))
).join(); ).join();
final Account managerAccount = accountsManager.get(uuid).get(); final Account managerAccount = accountsManager.get(uuid).orElseThrow();
final Account dynamoAccount = accountsDynamoDb.get(uuid).get(); final Account dynamoAccount = accounts.get(uuid).orElseThrow();
final Account redisAccount = getLastAccountFromRedisMock(commands); final Account redisAccount = getLastAccountFromRedisMock(commands);
@ -200,10 +190,9 @@ class AccountsManagerConcurrentModificationIntegrationTest {
new Pair<>("dynamo", dynamoAccount), new Pair<>("dynamo", dynamoAccount),
new Pair<>("redis", redisAccount) new Pair<>("redis", redisAccount)
).forEach(pair -> ).forEach(pair ->
verifyAccount(pair.first(), pair.second(), profileName, avatar, discoverableByPhoneNumber, verifyAccount(pair.first(), pair.second(), profileName, avatar, discoverableByPhoneNumber,
currentProfileVersion, identityKey, unidentifiedAccessKey, pin, registrationLock, currentProfileVersion, identityKey, unidentifiedAccessKey, pin, registrationLock,
unrestrictedUnidentifiedAccess, lastSeen) unrestrictedUnidentifiedAccess, lastSeen));
);
} }
private Account getLastAccountFromRedisMock(RedisAdvancedClusterCommands<String, String> commands) throws IOException { private Account getLastAccountFromRedisMock(RedisAdvancedClusterCommands<String, String> commands) throws IOException {
@ -220,9 +209,9 @@ class AccountsManagerConcurrentModificationIntegrationTest {
() -> assertEquals(profileName, account.getProfileName()), () -> assertEquals(profileName, account.getProfileName()),
() -> assertEquals(avatar, account.getAvatar()), () -> assertEquals(avatar, account.getAvatar()),
() -> assertEquals(discoverableByPhoneNumber, account.isDiscoverableByPhoneNumber()), () -> assertEquals(discoverableByPhoneNumber, account.isDiscoverableByPhoneNumber()),
() -> assertEquals(currentProfileVersion, account.getCurrentProfileVersion().get()), () -> assertEquals(currentProfileVersion, account.getCurrentProfileVersion().orElseThrow()),
() -> assertEquals(identityKey, account.getIdentityKey()), () -> assertEquals(identityKey, account.getIdentityKey()),
() -> assertArrayEquals(unidentifiedAccessKey, account.getUnidentifiedAccessKey().get()), () -> assertArrayEquals(unidentifiedAccessKey, account.getUnidentifiedAccessKey().orElseThrow()),
() -> assertTrue(account.getRegistrationLock().verify(clientRegistrationLock)), () -> assertTrue(account.getRegistrationLock().verify(clientRegistrationLock)),
() -> assertEquals(unrestrictedUnidentifiedAcces, account.isUnrestrictedUnidentifiedAccess()) () -> assertEquals(unrestrictedUnidentifiedAcces, account.isUnrestrictedUnidentifiedAccess())
); );
@ -231,7 +220,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
private CompletableFuture<?> modifyAccount(final UUID uuid, final Consumer<Account> accountMutation) { private CompletableFuture<?> modifyAccount(final UUID uuid, final Consumer<Account> accountMutation) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
final Account account = accountsManager.get(uuid).get(); final Account account = accountsManager.get(uuid).orElseThrow();
accountsManager.update(account, accountMutation); accountsManager.update(account, accountMutation);
}, mutationExecutor); }, mutationExecutor);
} }
@ -239,7 +228,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
private CompletableFuture<?> modifyDevice(final UUID uuid, final long deviceId, final Consumer<Device> deviceMutation) { private CompletableFuture<?> modifyDevice(final UUID uuid, final long deviceId, final Consumer<Device> deviceMutation) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
final Account account = accountsManager.get(uuid).get(); final Account account = accountsManager.get(uuid).orElseThrow();
accountsManager.updateDevice(account, deviceId, deviceMutation); accountsManager.updateDevice(account, deviceId, deviceMutation);
}, mutationExecutor); }, mutationExecutor);
} }

View File

@ -45,33 +45,35 @@ import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException; import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
class AccountsDynamoDbTest { class AccountsTest {
private static final String ACCOUNTS_TABLE_NAME = "accounts_test"; private static final String ACCOUNTS_TABLE_NAME = "accounts_test";
private static final String NUMBERS_TABLE_NAME = "numbers_test"; private static final String NUMBERS_TABLE_NAME = "numbers_test";
private static final int SCAN_PAGE_SIZE = 1;
@RegisterExtension @RegisterExtension
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder() static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
.tableName(ACCOUNTS_TABLE_NAME) .tableName(ACCOUNTS_TABLE_NAME)
.hashKey(AccountsDynamoDb.KEY_ACCOUNT_UUID) .hashKey(Accounts.KEY_ACCOUNT_UUID)
.attributeDefinition(AttributeDefinition.builder() .attributeDefinition(AttributeDefinition.builder()
.attributeName(AccountsDynamoDb.KEY_ACCOUNT_UUID) .attributeName(Accounts.KEY_ACCOUNT_UUID)
.attributeType(ScalarAttributeType.B) .attributeType(ScalarAttributeType.B)
.build()) .build())
.build(); .build();
private AccountsDynamoDb accountsDynamoDb; private Accounts accounts;
@BeforeEach @BeforeEach
void setupAccountsDao() { void setupAccountsDao() {
CreateTableRequest createNumbersTableRequest = CreateTableRequest.builder() CreateTableRequest createNumbersTableRequest = CreateTableRequest.builder()
.tableName(NUMBERS_TABLE_NAME) .tableName(NUMBERS_TABLE_NAME)
.keySchema(KeySchemaElement.builder() .keySchema(KeySchemaElement.builder()
.attributeName(AccountsDynamoDb.ATTR_ACCOUNT_E164) .attributeName(Accounts.ATTR_ACCOUNT_E164)
.keyType(KeyType.HASH) .keyType(KeyType.HASH)
.build()) .build())
.attributeDefinitions(AttributeDefinition.builder() .attributeDefinitions(AttributeDefinition.builder()
.attributeName(AccountsDynamoDb.ATTR_ACCOUNT_E164) .attributeName(Accounts.ATTR_ACCOUNT_E164)
.attributeType(ScalarAttributeType.S) .attributeType(ScalarAttributeType.S)
.build()) .build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT) .provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
@ -79,11 +81,11 @@ class AccountsDynamoDbTest {
dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest); dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest);
this.accountsDynamoDb = new AccountsDynamoDb( this.accounts = new Accounts(
dynamoDbExtension.getDynamoDbClient(), dynamoDbExtension.getDynamoDbClient(),
dynamoDbExtension.getTableName(), dynamoDbExtension.getTableName(),
NUMBERS_TABLE_NAME NUMBERS_TABLE_NAME,
); SCAN_PAGE_SIZE);
} }
@Test @Test
@ -91,12 +93,12 @@ class AccountsDynamoDbTest {
Device device = generateDevice (1 ); Device device = generateDevice (1 );
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device)); Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
boolean freshUser = accountsDynamoDb.create(account); boolean freshUser = accounts.create(account);
assertThat(freshUser).isTrue(); assertThat(freshUser).isTrue();
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account, true);
freshUser = accountsDynamoDb.create(account); freshUser = accounts.create(account);
assertThat(freshUser).isTrue(); assertThat(freshUser).isTrue();
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account, true);
@ -110,7 +112,7 @@ class AccountsDynamoDbTest {
Account account = generateAccount("+14151112222", UUID.randomUUID(), devices); Account account = generateAccount("+14151112222", UUID.randomUUID(), devices);
accountsDynamoDb.create(account); accounts.create(account);
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account, true);
} }
@ -131,11 +133,11 @@ class AccountsDynamoDbTest {
UUID uuidSecond = UUID.randomUUID(); UUID uuidSecond = UUID.randomUUID();
Account accountSecond = generateAccount("+14152221111", uuidSecond, devicesSecond); Account accountSecond = generateAccount("+14152221111", uuidSecond, devicesSecond);
accountsDynamoDb.create(accountFirst); accounts.create(accountFirst);
accountsDynamoDb.create(accountSecond); accounts.create(accountSecond);
Optional<Account> retrievedFirst = accountsDynamoDb.get("+14151112222"); Optional<Account> retrievedFirst = accounts.get("+14151112222");
Optional<Account> retrievedSecond = accountsDynamoDb.get("+14152221111"); Optional<Account> retrievedSecond = accounts.get("+14152221111");
assertThat(retrievedFirst.isPresent()).isTrue(); assertThat(retrievedFirst.isPresent()).isTrue();
assertThat(retrievedSecond.isPresent()).isTrue(); assertThat(retrievedSecond.isPresent()).isTrue();
@ -143,8 +145,8 @@ class AccountsDynamoDbTest {
verifyStoredState("+14151112222", uuidFirst, retrievedFirst.get(), accountFirst); verifyStoredState("+14151112222", uuidFirst, retrievedFirst.get(), accountFirst);
verifyStoredState("+14152221111", uuidSecond, retrievedSecond.get(), accountSecond); verifyStoredState("+14152221111", uuidSecond, retrievedSecond.get(), accountSecond);
retrievedFirst = accountsDynamoDb.get(uuidFirst); retrievedFirst = accounts.get(uuidFirst);
retrievedSecond = accountsDynamoDb.get(uuidSecond); retrievedSecond = accounts.get(uuidSecond);
assertThat(retrievedFirst.isPresent()).isTrue(); assertThat(retrievedFirst.isPresent()).isTrue();
assertThat(retrievedSecond.isPresent()).isTrue(); assertThat(retrievedSecond.isPresent()).isTrue();
@ -159,27 +161,27 @@ class AccountsDynamoDbTest {
UUID firstUuid = UUID.randomUUID(); UUID firstUuid = UUID.randomUUID();
Account account = generateAccount("+14151112222", firstUuid, Collections.singleton(device)); Account account = generateAccount("+14151112222", firstUuid, Collections.singleton(device));
accountsDynamoDb.create(account); accounts.create(account);
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account, true);
account.setProfileName("name"); account.setProfileName("name");
accountsDynamoDb.update(account); accounts.update(account);
UUID secondUuid = UUID.randomUUID(); UUID secondUuid = UUID.randomUUID();
device = generateDevice(1); device = generateDevice(1);
account = generateAccount("+14151112222", secondUuid, Collections.singleton(device)); account = generateAccount("+14151112222", secondUuid, Collections.singleton(device));
final boolean freshUser = accountsDynamoDb.create(account); final boolean freshUser = accounts.create(account);
assertThat(freshUser).isFalse(); assertThat(freshUser).isFalse();
verifyStoredState("+14151112222", firstUuid, account, true); verifyStoredState("+14151112222", firstUuid, account, true);
device = generateDevice(1); device = generateDevice(1);
Account invalidAccount = generateAccount("+14151113333", firstUuid, Collections.singleton(device)); Account invalidAccount = generateAccount("+14151113333", firstUuid, Collections.singleton(device));
assertThatThrownBy(() -> accountsDynamoDb.create(invalidAccount)); assertThatThrownBy(() -> accounts.create(invalidAccount));
} }
@Test @Test
@ -187,18 +189,18 @@ class AccountsDynamoDbTest {
Device device = generateDevice (1 ); Device device = generateDevice (1 );
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device)); Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
accountsDynamoDb.create(account); accounts.create(account);
device.setName("foobar"); device.setName("foobar");
accountsDynamoDb.update(account); accounts.update(account);
Optional<Account> retrieved = accountsDynamoDb.get("+14151112222"); Optional<Account> retrieved = accounts.get("+14151112222");
assertThat(retrieved.isPresent()).isTrue(); assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", account.getUuid(), retrieved.get(), account); verifyStoredState("+14151112222", account.getUuid(), retrieved.get(), account);
retrieved = accountsDynamoDb.get(account.getUuid()); retrieved = accounts.get(account.getUuid());
assertThat(retrieved.isPresent()).isTrue(); assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account, true);
@ -206,11 +208,11 @@ class AccountsDynamoDbTest {
device = generateDevice(1); device = generateDevice(1);
Account unknownAccount = generateAccount("+14151113333", UUID.randomUUID(), Collections.singleton(device)); Account unknownAccount = generateAccount("+14151113333", UUID.randomUUID(), Collections.singleton(device));
assertThatThrownBy(() -> accountsDynamoDb.update(unknownAccount)).isInstanceOfAny(ConditionalCheckFailedException.class); assertThatThrownBy(() -> accounts.update(unknownAccount)).isInstanceOfAny(ConditionalCheckFailedException.class);
account.setProfileName("name"); account.setProfileName("name");
accountsDynamoDb.update(account); accounts.update(account);
assertThat(account.getVersion()).isEqualTo(2); assertThat(account.getVersion()).isEqualTo(2);
@ -218,12 +220,12 @@ class AccountsDynamoDbTest {
account.setVersion(1); account.setVersion(1);
assertThatThrownBy(() -> accountsDynamoDb.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class); assertThatThrownBy(() -> accounts.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class);
account.setVersion(2); account.setVersion(2);
account.setProfileName("name2"); account.setProfileName("name2");
accountsDynamoDb.update(account); accounts.update(account);
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account, true);
} }
@ -232,8 +234,8 @@ class AccountsDynamoDbTest {
void testUpdateWithMockTransactionConflictException() { void testUpdateWithMockTransactionConflictException() {
final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class); final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class);
accountsDynamoDb = new AccountsDynamoDb(dynamoDbClient, accounts = new Accounts(dynamoDbClient,
dynamoDbExtension.getTableName(), NUMBERS_TABLE_NAME); dynamoDbExtension.getTableName(), NUMBERS_TABLE_NAME, SCAN_PAGE_SIZE);
when(dynamoDbClient.updateItem(any(UpdateItemRequest.class))) when(dynamoDbClient.updateItem(any(UpdateItemRequest.class)))
.thenThrow(TransactionConflictException.class); .thenThrow(TransactionConflictException.class);
@ -241,7 +243,7 @@ class AccountsDynamoDbTest {
Device device = generateDevice(1); Device device = generateDevice(1);
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device)); Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
assertThatThrownBy(() -> accountsDynamoDb.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class); assertThatThrownBy(() -> accounts.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class);
} }
@Test @Test
@ -251,12 +253,12 @@ class AccountsDynamoDbTest {
for (int i = 1; i <= 100; i++) { for (int i = 1; i <= 100; i++) {
Account account = generateAccount("+1" + String.format("%03d", i), UUID.randomUUID()); Account account = generateAccount("+1" + String.format("%03d", i), UUID.randomUUID());
users.add(account); users.add(account);
accountsDynamoDb.create(account); accounts.create(account);
} }
users.sort((account, t1) -> UUIDComparator.staticCompare(account.getUuid(), t1.getUuid())); users.sort((account, t1) -> UUIDComparator.staticCompare(account.getUuid(), t1.getUuid()));
AccountCrawlChunk retrieved = accountsDynamoDb.getAllFromStart(10, 1); AccountCrawlChunk retrieved = accounts.getAllFromStart(10);
assertThat(retrieved.getAccounts().size()).isEqualTo(10); assertThat(retrieved.getAccounts().size()).isEqualTo(10);
for (int i = 0; i < retrieved.getAccounts().size(); i++) { for (int i = 0; i < retrieved.getAccounts().size(); i++) {
@ -273,7 +275,7 @@ class AccountsDynamoDbTest {
} }
for (int j = 0; j < 9; j++) { for (int j = 0; j < 9; j++) {
retrieved = accountsDynamoDb.getAllFrom(retrieved.getLastUuid().orElseThrow(), 10, 1); retrieved = accounts.getAllFrom(retrieved.getLastUuid().orElseThrow(), 10);
assertThat(retrieved.getAccounts().size()).isEqualTo(10); assertThat(retrieved.getAccounts().size()).isEqualTo(10);
for (int i = 0; i < retrieved.getAccounts().size(); i++) { for (int i = 0; i < retrieved.getAccounts().size(); i++) {
@ -295,33 +297,36 @@ class AccountsDynamoDbTest {
@Test @Test
void testDelete() { void testDelete() {
final Device deletedDevice = generateDevice (1); final Device deletedDevice = generateDevice(1);
final Account deletedAccount = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(deletedDevice)); final Account deletedAccount = generateAccount("+14151112222", UUID.randomUUID(),
final Device retainedDevice = generateDevice (1); Collections.singleton(deletedDevice));
final Account retainedAccount = generateAccount("+14151112345", UUID.randomUUID(), Collections.singleton(retainedDevice)); final Device retainedDevice = generateDevice(1);
final Account retainedAccount = generateAccount("+14151112345", UUID.randomUUID(),
Collections.singleton(retainedDevice));
accountsDynamoDb.create(deletedAccount); accounts.create(deletedAccount);
accountsDynamoDb.create(retainedAccount); accounts.create(retainedAccount);
assertThat(accountsDynamoDb.get(deletedAccount.getUuid())).isPresent(); assertThat(accounts.get(deletedAccount.getUuid())).isPresent();
assertThat(accountsDynamoDb.get(retainedAccount.getUuid())).isPresent(); assertThat(accounts.get(retainedAccount.getUuid())).isPresent();
accountsDynamoDb.delete(deletedAccount.getUuid()); accounts.delete(deletedAccount.getUuid());
assertThat(accountsDynamoDb.get(deletedAccount.getUuid())).isNotPresent(); assertThat(accounts.get(deletedAccount.getUuid())).isNotPresent();
verifyStoredState(retainedAccount.getNumber(), retainedAccount.getUuid(), accountsDynamoDb.get(retainedAccount.getUuid()).get(), retainedAccount); verifyStoredState(retainedAccount.getNumber(), retainedAccount.getUuid(),
accounts.get(retainedAccount.getUuid()).get(), retainedAccount);
{ {
final Account recreatedAccount = generateAccount(deletedAccount.getNumber(), UUID.randomUUID(), final Account recreatedAccount = generateAccount(deletedAccount.getNumber(), UUID.randomUUID(),
Collections.singleton(generateDevice(1))); Collections.singleton(generateDevice(1)));
final boolean freshUser = accountsDynamoDb.create(recreatedAccount); final boolean freshUser = accounts.create(recreatedAccount);
assertThat(freshUser).isTrue(); assertThat(freshUser).isTrue();
assertThat(accountsDynamoDb.get(recreatedAccount.getUuid())).isPresent(); assertThat(accounts.get(recreatedAccount.getUuid())).isPresent();
verifyStoredState(recreatedAccount.getNumber(), recreatedAccount.getUuid(), verifyStoredState(recreatedAccount.getNumber(), recreatedAccount.getUuid(),
accountsDynamoDb.get(recreatedAccount.getUuid()).get(), recreatedAccount); accounts.get(recreatedAccount.getUuid()).get(), recreatedAccount);
} }
} }
@ -330,12 +335,12 @@ class AccountsDynamoDbTest {
Device device = generateDevice (1 ); Device device = generateDevice (1 );
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device)); Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device));
accountsDynamoDb.create(account); accounts.create(account);
Optional<Account> retrieved = accountsDynamoDb.get("+11111111"); Optional<Account> retrieved = accounts.get("+11111111");
assertThat(retrieved.isPresent()).isFalse(); assertThat(retrieved.isPresent()).isFalse();
retrieved = accountsDynamoDb.get(UUID.randomUUID()); retrieved = accounts.get(UUID.randomUUID());
assertThat(retrieved.isPresent()).isFalse(); assertThat(retrieved.isPresent()).isFalse();
} }
@ -357,7 +362,7 @@ class AccountsDynamoDbTest {
when(client.updateItem(any(UpdateItemRequest.class))) when(client.updateItem(any(UpdateItemRequest.class)))
.thenThrow(RuntimeException.class); .thenThrow(RuntimeException.class);
AccountsDynamoDb accounts = new AccountsDynamoDb(client, ACCOUNTS_TABLE_NAME, NUMBERS_TABLE_NAME); Accounts accounts = new Accounts(client, ACCOUNTS_TABLE_NAME, NUMBERS_TABLE_NAME, SCAN_PAGE_SIZE);
Account account = generateAccount("+14151112222", UUID.randomUUID()); Account account = generateAccount("+14151112222", UUID.randomUUID());
try { try {
@ -397,13 +402,13 @@ class AccountsDynamoDbTest {
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
Account account = generateAccount("+14151112222", uuid, Collections.singleton(device)); Account account = generateAccount("+14151112222", uuid, Collections.singleton(device));
account.setDiscoverableByPhoneNumber(false); account.setDiscoverableByPhoneNumber(false);
accountsDynamoDb.create(account); accounts.create(account);
verifyStoredState("+14151112222", account.getUuid(), account, false); verifyStoredState("+14151112222", account.getUuid(), account, false);
account.setDiscoverableByPhoneNumber(true); account.setDiscoverableByPhoneNumber(true);
accountsDynamoDb.update(account); accounts.update(account);
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account, true);
account.setDiscoverableByPhoneNumber(false); account.setDiscoverableByPhoneNumber(false);
accountsDynamoDb.update(account); accounts.update(account);
verifyStoredState("+14151112222", account.getUuid(), account, false); verifyStoredState("+14151112222", account.getUuid(), account, false);
} }
@ -433,20 +438,21 @@ class AccountsDynamoDbTest {
final GetItemResponse get = db.getItem(GetItemRequest.builder() final GetItemResponse get = db.getItem(GetItemRequest.builder()
.tableName(dynamoDbExtension.getTableName()) .tableName(dynamoDbExtension.getTableName())
.key(Map.of(AccountsDynamoDb.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid))) .key(Map.of(Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
.consistentRead(true) .consistentRead(true)
.build()); .build());
if (get.hasItem()) { if (get.hasItem()) {
String data = new String(get.item().get(AccountsDynamoDb.ATTR_ACCOUNT_DATA).b().asByteArray(), StandardCharsets.UTF_8); String data = new String(get.item().get(Accounts.ATTR_ACCOUNT_DATA).b().asByteArray(), StandardCharsets.UTF_8);
assertThat(data).isNotEmpty(); assertThat(data).isNotEmpty();
assertThat(AttributeValues.getInt(get.item(), AccountsDynamoDb.ATTR_VERSION, -1)) assertThat(AttributeValues.getInt(get.item(), Accounts.ATTR_VERSION, -1))
.isEqualTo(expecting.getVersion()); .isEqualTo(expecting.getVersion());
assertThat(AttributeValues.getBool(get.item(), AccountsDynamoDb.ATTR_CANONICALLY_DISCOVERABLE, !canonicallyDiscoverable)).isEqualTo(canonicallyDiscoverable); assertThat(AttributeValues.getBool(get.item(), Accounts.ATTR_CANONICALLY_DISCOVERABLE,
!canonicallyDiscoverable)).isEqualTo(canonicallyDiscoverable);
Account result = AccountsDynamoDb.fromItem(get.item()); Account result = Accounts.fromItem(get.item());
verifyStoredState(number, uuid, result, expecting); verifyStoredState(number, uuid, result, expecting);
} else { } else {
throw new AssertionError("No data"); throw new AssertionError("No data");

View File

@ -6,9 +6,7 @@
package org.whispersystems.textsecuregcm.tests.storage; package org.whispersystems.textsecuregcm.tests.storage;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
@ -26,20 +24,17 @@ import static org.mockito.Mockito.when;
import io.lettuce.core.RedisException; import io.lettuce.core.RedisException;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import java.io.IOException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
@ -48,7 +43,7 @@ import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue; import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsDynamoDb; import org.whispersystems.textsecuregcm.storage.Accounts;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ContestedOptimisticLockException; import org.whispersystems.textsecuregcm.storage.ContestedOptimisticLockException;
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager; import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
@ -60,12 +55,11 @@ import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager; import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.UsernamesManager; import org.whispersystems.textsecuregcm.storage.UsernamesManager;
import org.whispersystems.textsecuregcm.tests.util.JsonHelpers;
import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper; import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
class AccountsManagerTest { class AccountsManagerTest {
private AccountsDynamoDb accountsDynamoDb; private Accounts accounts;
private DeletedAccountsManager deletedAccountsManager; private DeletedAccountsManager deletedAccountsManager;
private DirectoryQueue directoryQueue; private DirectoryQueue directoryQueue;
private DynamicConfigurationManager dynamicConfigurationManager; private DynamicConfigurationManager dynamicConfigurationManager;
@ -86,7 +80,7 @@ class AccountsManagerTest {
@BeforeEach @BeforeEach
void setup() throws InterruptedException { void setup() throws InterruptedException {
accountsDynamoDb = mock(AccountsDynamoDb.class); accounts = mock(Accounts.class);
deletedAccountsManager = mock(DeletedAccountsManager.class); deletedAccountsManager = mock(DeletedAccountsManager.class);
directoryQueue = mock(DirectoryQueue.class); directoryQueue = mock(DirectoryQueue.class);
dynamicConfigurationManager = mock(DynamicConfigurationManager.class); dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
@ -107,7 +101,7 @@ class AccountsManagerTest {
}).when(deletedAccountsManager).lockAndTake(anyString(), any()); }).when(deletedAccountsManager).lockAndTake(anyString(), any());
accountsManager = new AccountsManager( accountsManager = new AccountsManager(
accountsDynamoDb, accounts,
RedisClusterHelper.buildMockRedisCluster(commands), RedisClusterHelper.buildMockRedisCluster(commands),
deletedAccountsManager, deletedAccountsManager,
directoryQueue, directoryQueue,
@ -117,8 +111,8 @@ class AccountsManagerTest {
profilesManager, profilesManager,
mock(StoredVerificationCodeManager.class), mock(StoredVerificationCodeManager.class),
mock(SecureStorageClient.class), mock(SecureStorageClient.class),
mock(SecureBackupClient.class), mock(SecureBackupClient.class)
dynamicConfigurationManager); );
} }
@Test @Test
@ -138,7 +132,7 @@ class AccountsManagerTest {
verify(commands, times(1)).get(eq("Account3::" + uuid)); verify(commands, times(1)).get(eq("Account3::" + uuid));
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verifyNoInteractions(accountsDynamoDb); verifyNoInteractions(accounts);
} }
@Test @Test
@ -157,7 +151,7 @@ class AccountsManagerTest {
verify(commands, times(1)).get(eq("Account3::" + uuid)); verify(commands, times(1)).get(eq("Account3::" + uuid));
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verifyNoInteractions(accountsDynamoDb); verifyNoInteractions(accounts);
} }
@ -168,7 +162,7 @@ class AccountsManagerTest {
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]);
when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(null); when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(null);
when(accountsDynamoDb.get(eq("+14152222222"))).thenReturn(Optional.of(account)); when(accounts.get(eq("+14152222222"))).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.get("+14152222222"); Optional<Account> retrieved = accountsManager.get("+14152222222");
@ -180,8 +174,8 @@ class AccountsManagerTest {
verify(commands, times(1)).set(eq("Account3::" + uuid), anyString()); verify(commands, times(1)).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verify(accountsDynamoDb, times(1)).get(eq("+14152222222")); verify(accounts, times(1)).get(eq("+14152222222"));
verifyNoMoreInteractions(accountsDynamoDb); verifyNoMoreInteractions(accounts);
} }
@Test @Test
@ -191,7 +185,7 @@ class AccountsManagerTest {
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]);
when(commands.get(eq("Account3::" + uuid))).thenReturn(null); when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accountsDynamoDb.get(eq(uuid))).thenReturn(Optional.of(account)); when(accounts.get(eq(uuid))).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.get(uuid); Optional<Account> retrieved = accountsManager.get(uuid);
@ -203,8 +197,8 @@ class AccountsManagerTest {
verify(commands, times(1)).set(eq("Account3::" + uuid), anyString()); verify(commands, times(1)).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verify(accountsDynamoDb, times(1)).get(eq(uuid)); verify(accounts, times(1)).get(eq(uuid));
verifyNoMoreInteractions(accountsDynamoDb); verifyNoMoreInteractions(accounts);
} }
@Test @Test
@ -213,7 +207,7 @@ class AccountsManagerTest {
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]);
when(commands.get(eq("AccountMap::+14152222222"))).thenThrow(new RedisException("Connection lost!")); when(commands.get(eq("AccountMap::+14152222222"))).thenThrow(new RedisException("Connection lost!"));
when(accountsDynamoDb.get(eq("+14152222222"))).thenReturn(Optional.of(account)); when(accounts.get(eq("+14152222222"))).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.get("+14152222222"); Optional<Account> retrieved = accountsManager.get("+14152222222");
@ -225,18 +219,17 @@ class AccountsManagerTest {
verify(commands, times(1)).set(eq("Account3::" + uuid), anyString()); verify(commands, times(1)).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verify(accountsDynamoDb, times(1)).get(eq("+14152222222")); verify(accounts, times(1)).get(eq("+14152222222"));
verifyNoMoreInteractions(accountsDynamoDb); verifyNoMoreInteractions(accounts);
} }
@Test @Test
void testGetAccountByUuidBrokenCache() { void testGetAccountByUuidBrokenCache() {
final boolean dynamoEnabled = true;
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]);
when(commands.get(eq("Account3::" + uuid))).thenThrow(new RedisException("Connection lost!")); when(commands.get(eq("Account3::" + uuid))).thenThrow(new RedisException("Connection lost!"));
when(accountsDynamoDb.get(eq(uuid))).thenReturn(Optional.of(account)); when(accounts.get(eq(uuid))).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.get(uuid); Optional<Account> retrieved = accountsManager.get(uuid);
@ -248,76 +241,8 @@ class AccountsManagerTest {
verify(commands, times(1)).set(eq("Account3::" + uuid), anyString()); verify(commands, times(1)).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verify(accountsDynamoDb, times(1)).get(eq(uuid)); verify(accounts, times(1)).get(eq(uuid));
verifyNoMoreInteractions(accountsDynamoDb); verifyNoMoreInteractions(accounts);
}
// TODO delete
@Disabled("migration specific")
@Test
void testUpdate_dynamoDbMigration() throws IOException {
UUID uuid = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]);
when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
// database fetches should always return new instances
when(accountsDynamoDb.get(uuid)).thenReturn(
Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16])));
when(accountsDynamoDb.get(uuid)).thenReturn(
Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16])));
doAnswer(ACCOUNT_UPDATE_ANSWER).when(accountsDynamoDb).update(any(Account.class));
Account updatedAccount = accountsManager.update(account, a -> a.setProfileName("name"));
assertThrows(AssertionError.class, account::getProfileName, "Account passed to update() should be stale");
assertNotSame(updatedAccount, account);
verify(accountsDynamoDb, times(1)).update(account);
verifyNoMoreInteractions(accountsDynamoDb);
ArgumentCaptor<Account> argumentCaptor = ArgumentCaptor.forClass(Account.class);
verify(accountsDynamoDb, times(1)).update(argumentCaptor.capture());
assertEquals(uuid, argumentCaptor.getValue().getUuid());
verify(accountsDynamoDb, times(1)).get(uuid);
verifyNoMoreInteractions(accountsDynamoDb);
ArgumentCaptor<String> redisSetArgumentCapture = ArgumentCaptor.forClass(String.class);
verify(commands, times(2)).set(anyString(), redisSetArgumentCapture.capture());
Account accountCached = JsonHelpers.fromJson(redisSetArgumentCapture.getAllValues().get(1), Account.class);
// uuid is @JsonIgnore, so we need to set it for compareAccounts to work
accountCached.setUuid(uuid);
assertEquals(Optional.empty(),
accountsManager.compareAccounts(Optional.of(updatedAccount), Optional.of(accountCached)));
}
// TODO delete
@Disabled("migration specific")
@Test
void testUpdate_dynamoMissing() {
UUID uuid = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]);
when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accountsDynamoDb.get(uuid)).thenReturn(Optional.empty());
doAnswer(ACCOUNT_UPDATE_ANSWER).when(accountsDynamoDb).update(any());
Account updatedAccount = accountsManager.update(account, a -> {
});
verify(accountsDynamoDb, times(1)).update(account);
verifyNoMoreInteractions(accountsDynamoDb);
verify(accountsDynamoDb, never()).update(account);
verify(accountsDynamoDb, times(1)).get(uuid);
verifyNoMoreInteractions(accountsDynamoDb);
assertEquals(1, updatedAccount.getVersion());
} }
@Test @Test
@ -327,52 +252,51 @@ class AccountsManagerTest {
when(commands.get(eq("Account3::" + uuid))).thenReturn(null); when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accountsDynamoDb.get(uuid)).thenReturn( when(accounts.get(uuid)).thenReturn(
Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16]))); Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16])));
doThrow(ContestedOptimisticLockException.class) doThrow(ContestedOptimisticLockException.class)
.doAnswer(ACCOUNT_UPDATE_ANSWER) .doAnswer(ACCOUNT_UPDATE_ANSWER)
.when(accountsDynamoDb).update(any()); .when(accounts).update(any());
when(accountsDynamoDb.get(uuid)).thenReturn( when(accounts.get(uuid)).thenReturn(
Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16]))); Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16])));
doThrow(ContestedOptimisticLockException.class) doThrow(ContestedOptimisticLockException.class)
.doAnswer(ACCOUNT_UPDATE_ANSWER) .doAnswer(ACCOUNT_UPDATE_ANSWER)
.when(accountsDynamoDb).update(any()); .when(accounts).update(any());
account = accountsManager.update(account, a -> a.setProfileName("name")); account = accountsManager.update(account, a -> a.setProfileName("name"));
assertEquals(1, account.getVersion()); assertEquals(1, account.getVersion());
assertEquals("name", account.getProfileName()); assertEquals("name", account.getProfileName());
verify(accountsDynamoDb, times(1)).get(uuid); verify(accounts, times(1)).get(uuid);
verify(accountsDynamoDb, times(2)).update(any()); verify(accounts, times(2)).update(any());
verifyNoMoreInteractions(accountsDynamoDb); verifyNoMoreInteractions(accounts);
} }
@Test @Test
void testUpdate_dynamoOptimisticLockingFailureDuringCreate() { void testUpdate_dynamoOptimisticLockingFailureDuringCreate() {
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]);
when(commands.get(eq("Account3::" + uuid))).thenReturn(null); when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accountsDynamoDb.get(uuid)).thenReturn(Optional.empty()) when(accounts.get(uuid)).thenReturn(Optional.empty())
.thenReturn(Optional.of(account)); .thenReturn(Optional.of(account));
when(accountsDynamoDb.create(any())).thenThrow(ContestedOptimisticLockException.class); when(accounts.create(any())).thenThrow(ContestedOptimisticLockException.class);
accountsManager.update(account, a -> {}); accountsManager.update(account, a -> {
});
verify(accountsDynamoDb, times(1)).update(account); verify(accounts, times(1)).update(account);
verifyNoMoreInteractions(accountsDynamoDb); verifyNoMoreInteractions(accounts);
} }
@Test @Test
void testUpdateDevice() { void testUpdateDevice() {
assertEquals(Optional.empty(), accountsManager.compareAccounts(Optional.empty(), Optional.empty()));
final UUID uuid = UUID.randomUUID(); final UUID uuid = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]);
when(accountsDynamoDb.get(uuid)).thenReturn( when(accounts.get(uuid)).thenReturn(
Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16]))); Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16])));
assertTrue(account.getDevices().isEmpty()); assertTrue(account.getDevices().isEmpty());
@ -400,90 +324,15 @@ class AccountsManagerTest {
verify(unknownDeviceUpdater, never()).accept(any(Device.class)); verify(unknownDeviceUpdater, never()).accept(any(Device.class));
} }
// TODO delete
@Disabled("migration specific")
@Test
void testCompareAccounts() throws Exception {
assertEquals(Optional.empty(), accountsManager.compareAccounts(Optional.empty(), Optional.empty()));
final UUID uuidA = UUID.randomUUID();
final Account a1 = new Account("+14152222222", uuidA, new HashSet<>(), new byte[16]);
assertEquals(Optional.of("primaryMissing"), accountsManager.compareAccounts(Optional.empty(), Optional.of(a1)));
final Account a2 = new Account("+14152222222", uuidA, new HashSet<>(), new byte[16]);
assertEquals(Optional.empty(), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
{
Device device1 = new Device();
device1.setId(1L);
a1.addDevice(device1);
assertEquals(Optional.of("devices"), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
Device device2 = new Device();
device2.setId(1L);
a2.addDevice(device2);
assertEquals(Optional.empty(), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
device1.setLastSeen(1L);
assertEquals(Optional.empty(), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
device1.setName("name");
assertEquals(Optional.of("devices"), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
device1.setName(null);
device1.setSignedPreKey(new SignedPreKey(1L, "123", "456"));
device2.setSignedPreKey(new SignedPreKey(2L, "123", "456"));
assertEquals(Optional.of("masterDeviceSignedPreKey"), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
device1.setSignedPreKey(null);
device2.setSignedPreKey(null);
assertEquals(Optional.empty(), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
device1.setApnId("123");
Thread.sleep(5);
device2.setApnId("123");
assertEquals(Optional.empty(), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
a1.removeDevice(1L);
a2.removeDevice(1L);
assertEquals(Optional.empty(), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
}
assertEquals(Optional.empty(), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
a1.setVersion(1);
assertEquals(Optional.of("version"), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
a2.setVersion(1);
a2.setProfileName("name");
assertEquals(Optional.of("profileName"), accountsManager.compareAccounts(Optional.of(a1), Optional.of(a2)));
}
@Test @Test
void testCreateFreshAccount() throws InterruptedException { void testCreateFreshAccount() throws InterruptedException {
when(accountsDynamoDb.create(any())).thenReturn(true); when(accounts.create(any())).thenReturn(true);
final String e164 = "+18005550123"; final String e164 = "+18005550123";
final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, null); final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, null);
accountsManager.create(e164, "password", null, attributes); accountsManager.create(e164, "password", null, attributes);
verify(accountsDynamoDb).create(argThat(account -> e164.equals(account.getNumber()))); verify(accounts).create(argThat(account -> e164.equals(account.getNumber())));
verifyNoInteractions(keys); verifyNoInteractions(keys);
verifyNoInteractions(messagesManager); verifyNoInteractions(messagesManager);
verifyNoInteractions(profilesManager); verifyNoInteractions(profilesManager);
@ -493,7 +342,7 @@ class AccountsManagerTest {
void testReregisterAccount() throws InterruptedException { void testReregisterAccount() throws InterruptedException {
final UUID existingUuid = UUID.randomUUID(); final UUID existingUuid = UUID.randomUUID();
when(accountsDynamoDb.create(any())).thenAnswer(invocation -> { when(accounts.create(any())).thenAnswer(invocation -> {
invocation.getArgument(0, Account.class).setUuid(existingUuid); invocation.getArgument(0, Account.class).setUuid(existingUuid);
return false; return false;
}); });
@ -502,7 +351,7 @@ class AccountsManagerTest {
final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, null); final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, null);
accountsManager.create(e164, "password", null, attributes); accountsManager.create(e164, "password", null, attributes);
verify(accountsDynamoDb).create( verify(accounts).create(
argThat(account -> e164.equals(account.getNumber()) && existingUuid.equals(account.getUuid()))); argThat(account -> e164.equals(account.getNumber()) && existingUuid.equals(account.getUuid())));
verify(keys).delete(existingUuid); verify(keys).delete(existingUuid);
verify(messagesManager).clear(existingUuid); verify(messagesManager).clear(existingUuid);
@ -519,13 +368,13 @@ class AccountsManagerTest {
return null; return null;
}).when(deletedAccountsManager).lockAndTake(anyString(), any()); }).when(deletedAccountsManager).lockAndTake(anyString(), any());
when(accountsDynamoDb.create(any())).thenReturn(true); when(accounts.create(any())).thenReturn(true);
final String e164 = "+18005550123"; final String e164 = "+18005550123";
final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, null); final AccountAttributes attributes = new AccountAttributes(false, 0, null, null, true, null);
accountsManager.create(e164, "password", null, attributes); accountsManager.create(e164, "password", null, attributes);
verify(accountsDynamoDb).create( verify(accounts).create(
argThat(account -> e164.equals(account.getNumber()) && recentlyDeletedUuid.equals(account.getUuid()))); argThat(account -> e164.equals(account.getNumber()) && recentlyDeletedUuid.equals(account.getUuid())));
verifyNoInteractions(keys); verifyNoInteractions(keys);
verifyNoInteractions(messagesManager); verifyNoInteractions(messagesManager);
@ -596,7 +445,7 @@ class AccountsManagerTest {
accountsManager.updateDeviceLastSeen(account, device, updatedLastSeen); accountsManager.updateDeviceLastSeen(account, device, updatedLastSeen);
assertEquals(expectUpdate ? updatedLastSeen : initialLastSeen, device.getLastSeen()); assertEquals(expectUpdate ? updatedLastSeen : initialLastSeen, device.getLastSeen());
verify(accountsDynamoDb, expectUpdate ? times(1) : never()).update(account); verify(accounts, expectUpdate ? times(1) : never()).update(account);
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")