Add basic support for phone number identifiers

This commit is contained in:
Jon Chambers 2021-11-09 10:23:08 -05:00 committed by GitHub
parent a1b925d1e0
commit 3398955c1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1406 additions and 452 deletions

@ -1 +1 @@
Subproject commit 56398ec89d0b719fb57bebaefac02dae85b3abb2 Subproject commit d20873c7d78eb0a33cb27d103ba6ee6807b09a88

View File

@ -171,6 +171,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty @JsonProperty
private AccountsDynamoDbConfiguration accountsDynamoDb; private AccountsDynamoDbConfiguration accountsDynamoDb;
@Valid
@NotNull
@JsonProperty
private DynamoDbConfiguration phoneNumberIdentifiersDynamoDb;
@Valid @Valid
@NotNull @NotNull
@JsonProperty @JsonProperty
@ -436,6 +441,10 @@ public class WhisperServerConfiguration extends Configuration {
return accountsDynamoDb; return accountsDynamoDb;
} }
public DynamoDbConfiguration getPhoneNumberIdentifiersDynamoDbConfiguration() {
return phoneNumberIdentifiersDynamoDb;
}
public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() { public DeletedAccountsDynamoDbConfiguration getDeletedAccountsDynamoDbConfiguration() {
return deletedAccountsDynamoDb; return deletedAccountsDynamoDb;
} }

View File

@ -186,6 +186,7 @@ import org.whispersystems.textsecuregcm.storage.MessagesCache;
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb; import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.NonNormalizedAccountCrawlerListener; import org.whispersystems.textsecuregcm.storage.NonNormalizedAccountCrawlerListener;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.Profiles; import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.PubSubManager; import org.whispersystems.textsecuregcm.storage.PubSubManager;
@ -330,6 +331,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(config.getAccountsDynamoDbConfiguration(), DynamoDbClient accountsDynamoDbClient = DynamoDbFromConfig.client(config.getAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create()); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
DynamoDbFromConfig.client(config.getPhoneNumberIdentifiersDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getDeletedAccountsDynamoDbConfiguration(), DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(config.getDeletedAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create()); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
@ -365,7 +370,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
Accounts accounts = new Accounts(accountsDynamoDbClient, Accounts accounts = new Accounts(accountsDynamoDbClient,
config.getAccountsDynamoDbConfiguration().getTableName(), config.getAccountsDynamoDbConfiguration().getTableName(),
config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(), config.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
config.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
config.getAccountsDynamoDbConfiguration().getScanPageSize()); config.getAccountsDynamoDbConfiguration().getScanPageSize());
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
config.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
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);
@ -465,9 +473,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(accounts, cacheCluster, deletedAccountsManager, AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, pendingAccountsManager, deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
secureStorageClient, secureBackupClient, clientPresenceManager, clock); pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
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

@ -76,7 +76,7 @@ public class AuthEnablementRefreshRequirementProvider implements WebsocketRefres
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled = @SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED); (Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
return accountsManager.get((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> { return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
final Set<Long> deviceIdsToDisplace; final Set<Long> deviceIdsToDisplace;
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account); final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);

View File

@ -78,7 +78,7 @@ public class BaseAccountAuthenticator {
deviceId = identifierAndDeviceId.second(); deviceId = identifierAndDeviceId.second();
} }
Optional<Account> account = accountsManager.get(accountUuid); Optional<Account> account = accountsManager.getByAccountIdentifier(accountUuid);
if (account.isEmpty()) { if (account.isEmpty()) {
failureReason = "noSuchAccount"; failureReason = "noSuchAccount";

View File

@ -8,6 +8,9 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
@NotNull @NotNull
private String phoneNumberTableName; private String phoneNumberTableName;
@NotNull
private String phoneNumberIdentifierTableName;
private int scanPageSize = 100; private int scanPageSize = 100;
@JsonProperty @JsonProperty
@ -15,6 +18,11 @@ public class AccountsDynamoDbConfiguration extends DynamoDbConfiguration {
return phoneNumberTableName; return phoneNumberTableName;
} }
@JsonProperty
public String getPhoneNumberIdentifierTableName() {
return phoneNumberIdentifierTableName;
}
@JsonProperty @JsonProperty
public int getScanPageSize() { public int getScanPageSize() {
return scanPageSize; return scanPageSize;

View File

@ -353,7 +353,7 @@ public class AccountController {
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid) storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
.ifPresent(smsSender::reportVerificationSucceeded); .ifPresent(smsSender::reportVerificationSucceeded);
Optional<Account> existingAccount = accounts.get(number); Optional<Account> existingAccount = accounts.getByE164(number);
if (existingAccount.isPresent()) { if (existingAccount.isPresent()) {
verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock()); verifyRegistrationLock(existingAccount.get(), accountAttributes.getRegistrationLock());
@ -412,7 +412,7 @@ public class AccountController {
storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid) storedVerificationCode.flatMap(StoredVerificationCode::getTwilioVerificationSid)
.ifPresent(smsSender::reportVerificationSucceeded); .ifPresent(smsSender::reportVerificationSucceeded);
final Optional<Account> existingAccount = accounts.get(request.getNumber()); final Optional<Account> existingAccount = accounts.getByE164(request.getNumber());
if (existingAccount.isPresent()) { if (existingAccount.isPresent()) {
verifyRegistrationLock(existingAccount.get(), request.getRegistrationLock()); verifyRegistrationLock(existingAccount.get(), request.getRegistrationLock());

View File

@ -168,7 +168,7 @@ public class DeviceController {
throw new WebApplicationException(Response.status(403).build()); throw new WebApplicationException(Response.status(403).build());
} }
Optional<Account> account = accounts.get(number); Optional<Account> account = accounts.getByE164(number);
if (!account.isPresent()) { if (!account.isPresent()) {
throw new WebApplicationException(Response.status(403).build()); throw new WebApplicationException(Response.status(403).build());

View File

@ -159,7 +159,7 @@ public class DonationController {
@Override @Override
public boolean block() { public boolean block() {
final Optional<Account> optionalAccount = accountsManager.get(auth.getAccount().getUuid()); final Optional<Account> optionalAccount = accountsManager.getByAccountIdentifier(auth.getAccount().getUuid());
optionalAccount.ifPresent(account -> { optionalAccount.ifPresent(account -> {
accountsManager.update(account, a -> { accountsManager.update(account, a -> {
a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible())); a.addBadge(clock, new AccountBadge(badgeId, receiptExpiration, request.isVisible()));

View File

@ -129,7 +129,7 @@ public class KeysController {
final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount); final Optional<Account> account = auth.map(AuthenticatedAccount::getAccount);
Optional<Account> target = accounts.get(targetUuid); Optional<Account> target = accounts.getByAccountIdentifier(targetUuid);
OptionalAccess.verify(account, accessKey, target, deviceId); OptionalAccess.verify(account, accessKey, target, deviceId);
assert (target.isPresent()); assert (target.isPresent());

View File

@ -215,7 +215,7 @@ public class MessageController {
Optional<Account> destination; Optional<Account> destination;
if (!isSyncMessage) { if (!isSyncMessage) {
destination = accountsManager.get(destinationUuid); destination = accountsManager.getByAccountIdentifier(destinationUuid);
} else { } else {
destination = source.map(AuthenticatedAccount::getAccount); destination = source.map(AuthenticatedAccount::getAccount);
} }
@ -311,7 +311,7 @@ public class MessageController {
.map(Recipient::getUuid) .map(Recipient::getUuid)
.distinct() .distinct()
.collect(Collectors.toUnmodifiableMap(Function.identity(), uuid -> { .collect(Collectors.toUnmodifiableMap(Function.identity(), uuid -> {
Optional<Account> account = accountsManager.get(uuid); Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
if (account.isEmpty()) { if (account.isEmpty()) {
throw new WebApplicationException(Status.NOT_FOUND); throw new WebApplicationException(Status.NOT_FOUND);
} }

View File

@ -251,7 +251,7 @@ public class ProfileController {
isSelf = uuid.equals(authedUuid); isSelf = uuid.equals(authedUuid);
} }
Optional<Account> accountProfile = accountsManager.get(uuid); Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid);
OptionalAccess.verify(requestAccount, accessKey, accountProfile); OptionalAccess.verify(requestAccount, accessKey, accountProfile);
assert(accountProfile.isPresent()); assert(accountProfile.isPresent());
@ -316,7 +316,7 @@ public class ProfileController {
final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get()); final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get());
Optional<Account> accountProfile = accountsManager.get(uuid.get()); Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(uuid.get());
if (accountProfile.isEmpty()) { if (accountProfile.isEmpty()) {
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()); throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
@ -398,7 +398,7 @@ public class ProfileController {
isSelf = authedUuid.equals(identifier); isSelf = authedUuid.equals(identifier);
} }
Optional<Account> accountProfile = accountsManager.get(identifier); Optional<Account> accountProfile = accountsManager.getByAccountIdentifier(identifier);
OptionalAccess.verify(auth.map(AuthenticatedAccount::getAccount), accessKey, accountProfile); OptionalAccess.verify(auth.map(AuthenticatedAccount::getAccount), accessKey, accountProfile);
Optional<String> username = usernamesManager.get(accountProfile.get().getUuid()); Optional<String> username = usernamesManager.get(accountProfile.get().getUuid());

View File

@ -125,7 +125,7 @@ public class APNSender implements Managed {
private void handleUnregisteredUser(String registrationId, UUID uuid, long deviceId) { private void handleUnregisteredUser(String registrationId, UUID uuid, long deviceId) {
// logger.info("Got APN Unregistered: " + number + "," + deviceId); // logger.info("Got APN Unregistered: " + number + "," + deviceId);
Optional<Account> account = accountsManager.get(uuid); Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
if (account.isEmpty()) { if (account.isEmpty()) {
logger.info("No account found: {}", uuid); logger.info("No account found: {}", uuid);

View File

@ -99,7 +99,7 @@ public class ApnFallbackManager implements Managed {
final Optional<Account> maybeAccount = separated.map(Pair::first) final Optional<Account> maybeAccount = separated.map(Pair::first)
.map(UUID::fromString) .map(UUID::fromString)
.flatMap(accountsManager::get); .flatMap(accountsManager::getByAccountIdentifier);
final Optional<Device> maybeDevice = separated.map(Pair::second) final Optional<Device> maybeDevice = separated.map(Pair::second)
.flatMap(deviceId -> maybeAccount.flatMap(account -> account.getDevice(deviceId))); .flatMap(deviceId -> maybeAccount.flatMap(account -> account.getDevice(deviceId)));

View File

@ -139,7 +139,7 @@ public class GCMSender {
} }
private Optional<Account> getAccountForEvent(GcmMessage message) { private Optional<Account> getAccountForEvent(GcmMessage message) {
Optional<Account> account = message.getUuid().flatMap(accountsManager::get); Optional<Account> account = message.getUuid().flatMap(accountsManager::getByAccountIdentifier);
if (account.isPresent()) { if (account.isPresent()) {
Optional<Device> device = account.get().getDevice(message.getDeviceId()); Optional<Device> device = account.get().getDevice(message.getDeviceId());

View File

@ -33,7 +33,7 @@ public class ReceiptSender {
return; return;
} }
final Account destinationAccount = accountManager.get(destinationUuid) final Account destinationAccount = accountManager.getByAccountIdentifier(destinationUuid)
.orElseThrow(() -> new NoSuchUserException(destinationUuid)); .orElseThrow(() -> new NoSuchUserException(destinationUuid));
final Envelope.Builder message = Envelope.newBuilder() final Envelope.Builder message = Envelope.newBuilder()

View File

@ -23,6 +23,7 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock; import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import javax.annotation.Nullable;
public class Account { public class Account {
@ -32,6 +33,11 @@ public class Account {
@JsonIgnore @JsonIgnore
private UUID uuid; private UUID uuid;
// Nullable only until initial migration is complete
@JsonProperty("pni")
@Nullable
private UUID phoneNumberIdentifier;
@JsonProperty @JsonProperty
private String number; private String number;
@ -80,9 +86,10 @@ public class Account {
public Account() {} public Account() {}
@VisibleForTesting @VisibleForTesting
public Account(String number, UUID uuid, Set<Device> devices, byte[] unidentifiedAccessKey) { public Account(String number, UUID uuid, final UUID phoneNumberIdentifier, Set<Device> devices, byte[] unidentifiedAccessKey) {
this.number = number; this.number = number;
this.uuid = uuid; this.uuid = uuid;
this.phoneNumberIdentifier = phoneNumberIdentifier;
this.devices = devices; this.devices = devices;
this.unidentifiedAccessKey = unidentifiedAccessKey; this.unidentifiedAccessKey = unidentifiedAccessKey;
} }
@ -98,16 +105,11 @@ public class Account {
this.uuid = uuid; this.uuid = uuid;
} }
public void setNumber(String number) { // Optional only until initial migration is complete
public Optional<UUID> getPhoneNumberIdentifier() {
requireNotStale(); requireNotStale();
this.number = number; return Optional.ofNullable(phoneNumberIdentifier);
}
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
requireNotStale();
this.canonicallyDiscoverable = canonicallyDiscoverable;
} }
public String getNumber() { public String getNumber() {
@ -116,6 +118,13 @@ public class Account {
return number; return number;
} }
public void setNumber(String number, UUID phoneNumberIdentifier) {
requireNotStale();
this.number = number;
this.phoneNumberIdentifier = phoneNumberIdentifier;
}
public void addDevice(Device device) { public void addDevice(Device device) {
requireNotStale(); requireNotStale();
@ -247,6 +256,12 @@ public class Account {
return canonicallyDiscoverable; return canonicallyDiscoverable;
} }
public void setCanonicallyDiscoverable(boolean canonicallyDiscoverable) {
requireNotStale();
this.canonicallyDiscoverable = canonicallyDiscoverable;
}
public Optional<String> getRelay() { public Optional<String> getRelay() {
requireNotStale(); requireNotStale();

View File

@ -13,23 +13,24 @@ import io.micrometer.core.instrument.Timer;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.AttributeValues; import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.SystemMapper; import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.UUIDUtil; import org.whispersystems.textsecuregcm.util.UUIDUtil;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.CancellationReason; import software.amazon.awssdk.services.dynamodb.model.CancellationReason;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.Delete; import software.amazon.awssdk.services.dynamodb.model.Delete;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.Put; import software.amazon.awssdk.services.dynamodb.model.Put;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest; import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem; import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
@ -37,13 +38,13 @@ import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException; import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException;
import software.amazon.awssdk.services.dynamodb.model.Update; import software.amazon.awssdk.services.dynamodb.model.Update;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
public class Accounts extends AbstractDynamoDbStore { 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";
// uuid, attribute on account table, primary key for PNI table
static final String ATTR_PNI_UUID = "PNI";
// phone number // phone number
static final String ATTR_ACCOUNT_E164 = "P"; static final String ATTR_ACCOUNT_E164 = "P";
// account, serialized to JSON // account, serialized to JSON
@ -55,7 +56,8 @@ public class Accounts extends AbstractDynamoDbStore {
private final DynamoDbClient client; private final DynamoDbClient client;
private final String phoneNumbersTableName; private final String phoneNumberConstraintTableName;
private final String phoneNumberIdentifierConstraintTableName;
private final String accountsTableName; private final String accountsTableName;
private final int scanPageSize; private final int scanPageSize;
@ -64,19 +66,22 @@ public class Accounts extends AbstractDynamoDbStore {
private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber")); private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update")); private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber")); private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid")); private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom")); 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 GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete")); private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
private static final Logger log = LoggerFactory.getLogger(Accounts.class);
public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumbersTableName, public Accounts(DynamoDbClient client, String accountsTableName, String phoneNumberConstraintTableName,
final int scanPageSize) { String phoneNumberIdentifierConstraintTableName, final int scanPageSize) {
super(client); super(client);
this.client = client; this.client = client;
this.phoneNumbersTableName = phoneNumbersTableName; this.phoneNumberConstraintTableName = phoneNumberConstraintTableName;
this.phoneNumberIdentifierConstraintTableName = phoneNumberIdentifierConstraintTableName;
this.accountsTableName = accountsTableName; this.accountsTableName = accountsTableName;
this.scanPageSize = scanPageSize; this.scanPageSize = scanPageSize;
} }
@ -85,35 +90,98 @@ public class Accounts extends AbstractDynamoDbStore {
return CREATE_TIMER.record(() -> { return CREATE_TIMER.record(() -> {
try { try {
TransactWriteItem phoneNumberConstraintPut = buildPutWriteItemForPhoneNumberConstraint(account, account.getUuid()); TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
TransactWriteItem accountPut = buildPutWriteItemForAccount(account, account.getUuid(), Put.builder() .put(
.conditionExpression("attribute_not_exists(#number) OR #number = :number") Put.builder()
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164)) .tableName(phoneNumberConstraintTableName)
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))); .item(Map.of(
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", KEY_ACCOUNT_UUID,
"#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
assert account.getPhoneNumberIdentifier().isPresent();
if (account.getPhoneNumberIdentifier().isEmpty()) {
log.error("Account {} is missing a phone number identifier", account.getUuid());
}
TransactWriteItem phoneNumberIdentifierConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.item(Map.of(
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier().get()),
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", KEY_ACCOUNT_UUID,
"#pni", ATTR_PNI_UUID))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
account.getPhoneNumberIdentifier().ifPresent(pni -> item.put(ATTR_PNI_UUID, AttributeValues.fromUUID(pni)));
TransactWriteItem accountPut = TransactWriteItem.builder()
.put(Put.builder()
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
.tableName(accountsTableName)
.item(item)
.build())
.build();
final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder() final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(phoneNumberConstraintPut, accountPut) .transactItems(phoneNumberConstraintPut, phoneNumberIdentifierConstraintPut, accountPut)
.build(); .build();
try { try {
client.transactWriteItems(request); client.transactWriteItems(request);
} catch (TransactionCanceledException e) { } catch (TransactionCanceledException e) {
final CancellationReason accountCancellationReason = e.cancellationReasons().get(1); final CancellationReason accountCancellationReason = e.cancellationReasons().get(2);
if ("ConditionalCheckFailed".equals(accountCancellationReason.code())) { if ("ConditionalCheckFailed".equals(accountCancellationReason.code())) {
throw new IllegalArgumentException("uuid present with different phone number"); throw new IllegalArgumentException("account identifier present with different phone number");
} }
final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0); final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0);
final CancellationReason phoneNumberIdentifierConstraintCancellationReason = e.cancellationReasons().get(1);
if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code())) { if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ||
"ConditionalCheckFailed".equals(phoneNumberIdentifierConstraintCancellationReason.code())) {
ByteBuffer actualAccountUuid = phoneNumberConstraintCancellationReason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer(); // In theory, both reasons should trip in tandem and either should give us the information we need. Even so,
// we'll be cautious here and make sure we're choosing a condition check that really failed.
final CancellationReason reason = "ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ?
phoneNumberConstraintCancellationReason : phoneNumberIdentifierConstraintCancellationReason;
ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid)); account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid));
final int version = get(account.getUuid()).get().getVersion(); final Account existingAccount = getByAccountIdentifier(account.getUuid()).orElseThrow();
account.setVersion(version); account.setNumber(existingAccount.getNumber(), existingAccount.getPhoneNumberIdentifier().orElse(account.getPhoneNumberIdentifier().orElseThrow()));
account.setVersion(existingAccount.getVersion());
update(account); update(account);
@ -125,7 +193,7 @@ public class Accounts extends AbstractDynamoDbStore {
throw new ContestedOptimisticLockException(); throw new ContestedOptimisticLockException();
} }
// this shouldnt happen // this shouldn't happen
throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e)); throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e));
} }
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
@ -136,44 +204,11 @@ public class Accounts extends AbstractDynamoDbStore {
}); });
} }
private TransactWriteItem buildPutWriteItemForAccount(Account account, UUID uuid, Put.Builder putBuilder) throws JsonProcessingException {
return TransactWriteItem.builder()
.put(putBuilder
.tableName(accountsTableName)
.item(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())))
.build())
.build();
}
private TransactWriteItem buildPutWriteItemForPhoneNumberConstraint(Account account, UUID uuid) {
return TransactWriteItem.builder()
.put(
Put.builder()
.tableName(phoneNumbersTableName)
.item(Map.of(
ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", KEY_ACCOUNT_UUID,
"#number", ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(uuid)))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
}
/** /**
* Changes the phone number for the given account. The given account's number should be its current, pre-change * Changes the phone number for the given account. The given account's number should be its current, pre-change
* number. If this method succeeds, the account's number will be changed to the new number. If the update fails for * number. If this method succeeds, the account's number will be changed to the new number and its phone number
* any reason, the account's number will be unchanged. * identifier will be changed to the given phone number identifier. If the update fails for any reason, the account's
* number and PNI will be unchanged.
* <p/> * <p/>
* This method expects that any accounts with conflicting numbers will have been removed by the time this method is * This method expects that any accounts with conflicting numbers will have been removed by the time this method is
* called. This method may fail with an unspecified {@link RuntimeException} if another account with the same number * called. This method may fail with an unspecified {@link RuntimeException} if another account with the same number
@ -182,26 +217,28 @@ public class Accounts extends AbstractDynamoDbStore {
* @param account the account for which to change the phone number * @param account the account for which to change the phone number
* @param number the new phone number * @param number the new phone number
*/ */
public void changeNumber(final Account account, final String number) { public void changeNumber(final Account account, final String number, final UUID phoneNumberIdentifier) {
CHANGE_NUMBER_TIMER.record(() -> { CHANGE_NUMBER_TIMER.record(() -> {
final String originalNumber = account.getNumber(); final String originalNumber = account.getNumber();
final Optional<UUID> originalPni = account.getPhoneNumberIdentifier();
boolean succeeded = false; boolean succeeded = false;
account.setNumber(number); account.setNumber(number, phoneNumberIdentifier);
try { try {
final List<TransactWriteItem> writeItems = new ArrayList<>(); final List<TransactWriteItem> writeItems = new ArrayList<>();
writeItems.add(TransactWriteItem.builder() writeItems.add(TransactWriteItem.builder()
.delete(Delete.builder() .delete(Delete.builder()
.tableName(phoneNumbersTableName) .tableName(phoneNumberConstraintTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(originalNumber))) .key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(originalNumber)))
.build()) .build())
.build()); .build());
writeItems.add(TransactWriteItem.builder() writeItems.add(TransactWriteItem.builder()
.put(Put.builder() .put(Put.builder()
.tableName(phoneNumbersTableName) .tableName(phoneNumberConstraintTableName)
.item(Map.of( .item(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()), KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
ATTR_ACCOUNT_E164, AttributeValues.fromString(number))) ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
@ -211,20 +248,41 @@ public class Accounts extends AbstractDynamoDbStore {
.build()) .build())
.build()); .build());
originalPni.ifPresent(pni -> writeItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(pni)))
.build())
.build()));
writeItems.add(TransactWriteItem.builder()
.put(Put.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.item(Map.of(
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.conditionExpression("attribute_not_exists(#pni)")
.expressionAttributeNames(Map.of("#pni", ATTR_PNI_UUID))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build());
writeItems.add( writeItems.add(
TransactWriteItem.builder() TransactWriteItem.builder()
.update(Update.builder() .update(Update.builder()
.tableName(accountsTableName) .tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()))) .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.updateExpression("SET #data = :data, #number = :number, #cds = :cds ADD #version :version_increment") .updateExpression("SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment")
.conditionExpression("attribute_exists(#number) AND #version = :version") .conditionExpression("attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164, .expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA, "#data", ATTR_ACCOUNT_DATA,
"#cds", ATTR_CANONICALLY_DISCOVERABLE, "#cds", ATTR_CANONICALLY_DISCOVERABLE,
"#pni", ATTR_PNI_UUID,
"#version", ATTR_VERSION)) "#version", ATTR_VERSION))
.expressionAttributeValues(Map.of( .expressionAttributeValues(Map.of(
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)), ":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":number", AttributeValues.fromString(number), ":number", AttributeValues.fromString(number),
":pni", AttributeValues.fromUUID(phoneNumberIdentifier),
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()), ":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
":version", AttributeValues.fromInt(account.getVersion()), ":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1))) ":version_increment", AttributeValues.fromInt(1)))
@ -243,7 +301,7 @@ public class Accounts extends AbstractDynamoDbStore {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} finally { } finally {
if (!succeeded) { if (!succeeded) {
account.setNumber(originalNumber); account.setNumber(originalNumber, originalPni.orElse(null));
} }
} }
}); });
@ -251,57 +309,121 @@ public class Accounts extends AbstractDynamoDbStore {
public void update(Account account) throws ContestedOptimisticLockException { public void update(Account account) throws ContestedOptimisticLockException {
UPDATE_TIMER.record(() -> { UPDATE_TIMER.record(() -> {
UpdateItemRequest updateItemRequest; final List<TransactWriteItem> transactWriteItems = new ArrayList<>(2);
try {
updateItemRequest = UpdateItemRequest.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
.conditionExpression("attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA,
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
"#version", ATTR_VERSION))
.expressionAttributeValues(Map.of(
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1)))
.returnValues(ReturnValue.UPDATED_NEW)
.build();
try {
final TransactWriteItem updateAccountWriteItem;
if (account.getPhoneNumberIdentifier().isPresent()) {
updateAccountWriteItem = TransactWriteItem.builder()
.update(Update.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.updateExpression("SET #data = :data, #cds = :cds, #pni = :pni ADD #version :version_increment")
.conditionExpression("attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA,
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
"#version", ATTR_VERSION,
"#pni", ATTR_PNI_UUID))
.expressionAttributeValues(Map.of(
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1),
":pni", AttributeValues.fromUUID(account.getPhoneNumberIdentifier().get())))
.build())
.build();
} else {
updateAccountWriteItem = TransactWriteItem.builder()
.update(Update.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.updateExpression("SET #data = :data, #cds = :cds ADD #version :version_increment")
.conditionExpression("attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
"#data", ATTR_ACCOUNT_DATA,
"#cds", ATTR_CANONICALLY_DISCOVERABLE,
"#version", ATTR_VERSION))
.expressionAttributeValues(Map.of(
":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1)))
.build())
.build();
}
transactWriteItems.add(updateAccountWriteItem);
// TODO Remove after initial migration to phone number identifiers
account.getPhoneNumberIdentifier().ifPresent(phoneNumberIdentifier -> transactWriteItems.add(
TransactWriteItem.builder()
.put(Put.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.item(Map.of(
ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier().get()),
KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression("attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
.expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID, "#pni", ATTR_PNI_UUID))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build()));
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
try { try {
UpdateItemResponse response = client.updateItem(updateItemRequest); client.transactWriteItems(TransactWriteItemsRequest.builder()
.transactItems(transactWriteItems)
.build());
account.setVersion(AttributeValues.getInt(response.attributes(), "V", account.getVersion() + 1)); account.setVersion(account.getVersion() + 1);
} catch (final TransactionConflictException e) { } catch (final TransactionConflictException e) {
throw new ContestedOptimisticLockException(); throw new ContestedOptimisticLockException();
} catch (final ConditionalCheckFailedException e) { } catch (final TransactionCanceledException e) {
// the exception doesnt give details about which condition failed, if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(1).code())) {
// but we can infer it was an optimistic locking failure if the UUID is known log.error("Conflicting phone number mapping exists for account {}, PNI {}", account.getUuid(), account.getPhoneNumberIdentifier());
throw get(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e; throw e;
}
// We can infer an optimistic locking failure if the UUID is known
throw getByAccountIdentifier(account.getUuid()).isPresent() ? new ContestedOptimisticLockException() : e;
} }
}); });
} }
public Optional<Account> get(String number) { public Optional<Account> getByE164(String number) {
return GET_BY_NUMBER_TIMER.record(() -> { return GET_BY_NUMBER_TIMER.record(() -> {
final GetItemResponse response = client.getItem(GetItemRequest.builder() final GetItemResponse response = client.getItem(GetItemRequest.builder()
.tableName(phoneNumbersTableName) .tableName(phoneNumberConstraintTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(number))) .key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
.build()); .build());
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(this::accountByUuid)
.map(Accounts::fromItem);
});
}
public Optional<Account> getByPhoneNumberIdentifier(final UUID phoneNumberIdentifier) {
return GET_BY_PNI_TIMER.record(() -> {
final GetItemResponse response = client.getItem(GetItemRequest.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.build());
return Optional.ofNullable(response.item())
.map(item -> item.get(KEY_ACCOUNT_UUID))
.map(this::accountByUuid)
.map(Accounts::fromItem); .map(Accounts::fromItem);
}); });
} }
@ -315,7 +437,7 @@ public class Accounts extends AbstractDynamoDbStore {
return r.item().isEmpty() ? null : r.item(); return r.item().isEmpty() ? null : r.item();
} }
public Optional<Account> get(UUID uuid) { public Optional<Account> getByAccountIdentifier(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(Accounts::fromItem)); .map(Accounts::fromItem));
@ -324,13 +446,11 @@ public class Accounts extends AbstractDynamoDbStore {
public void delete(UUID uuid) { public void delete(UUID uuid) {
DELETE_TIMER.record(() -> { DELETE_TIMER.record(() -> {
Optional<Account> maybeAccount = get(uuid); getByAccountIdentifier(uuid).ifPresent(account -> {
maybeAccount.ifPresent(account -> {
TransactWriteItem phoneNumberDelete = TransactWriteItem.builder() TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
.delete(Delete.builder() .delete(Delete.builder()
.tableName(phoneNumbersTableName) .tableName(phoneNumberConstraintTableName)
.key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()))) .key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
.build()) .build())
.build(); .build();
@ -342,8 +462,17 @@ public class Accounts extends AbstractDynamoDbStore {
.build()) .build())
.build(); .build();
final List<TransactWriteItem> transactWriteItems = new ArrayList<>(List.of(phoneNumberDelete, accountDelete));
account.getPhoneNumberIdentifier().ifPresent(pni -> transactWriteItems.add(TransactWriteItem.builder()
.delete(Delete.builder()
.tableName(phoneNumberIdentifierConstraintTableName)
.key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(pni)))
.build())
.build()));
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder() TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(phoneNumberDelete, accountDelete).build(); .transactItems(transactWriteItems).build();
client.transactWriteItems(request); client.transactWriteItems(request);
}); });
@ -393,7 +522,7 @@ public class Accounts extends AbstractDynamoDbStore {
} }
try { try {
Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class); Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
account.setNumber(item.get(ATTR_ACCOUNT_E164).s()); account.setNumber(item.get(ATTR_ACCOUNT_E164).s(), AttributeValues.getUUID(item, ATTR_PNI_UUID, null));
account.setUuid(UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer())); account.setUuid(UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer()));
account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n())); account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE)).map(av -> av.bool()).orElse(false)); account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE)).map(av -> av.bool()).orElse(false));

View File

@ -52,6 +52,7 @@ public class AccountsManager {
private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet" )); private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet" ));
private static final Timer redisNumberGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisNumberGet")); private static final Timer redisNumberGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisNumberGet"));
private static final Timer redisPniGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisPniGet"));
private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet" )); private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet" ));
private static final Timer redisDeleteTimer = metricRegistry.timer(name(AccountsManager.class, "redisDelete" )); private static final Timer redisDeleteTimer = metricRegistry.timer(name(AccountsManager.class, "redisDelete" ));
@ -63,18 +64,19 @@ public class AccountsManager {
private final Logger logger = LoggerFactory.getLogger(AccountsManager.class); private final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
private final Accounts accounts; private final Accounts accounts;
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
private final FaultTolerantRedisCluster cacheCluster; private final FaultTolerantRedisCluster cacheCluster;
private final DeletedAccountsManager deletedAccountsManager; private final DeletedAccountsManager deletedAccountsManager;
private final DirectoryQueue directoryQueue; private final DirectoryQueue directoryQueue;
private final KeysDynamoDb keysDynamoDb; private final KeysDynamoDb keysDynamoDb;
private final MessagesManager messagesManager; private final MessagesManager messagesManager;
private final UsernamesManager usernamesManager; private final UsernamesManager usernamesManager;
private final ProfilesManager profilesManager; private final ProfilesManager profilesManager;
private final StoredVerificationCodeManager pendingAccounts; private final StoredVerificationCodeManager pendingAccounts;
private final SecureStorageClient secureStorageClient; private final SecureStorageClient secureStorageClient;
private final SecureBackupClient secureBackupClient; private final SecureBackupClient secureBackupClient;
private final ClientPresenceManager clientPresenceManager; private final ClientPresenceManager clientPresenceManager;
private final ObjectMapper mapper; private final ObjectMapper mapper;
private final Clock clock; private final Clock clock;
public enum DeletionReason { public enum DeletionReason {
@ -89,10 +91,13 @@ public class AccountsManager {
} }
} }
public AccountsManager(Accounts accounts, FaultTolerantRedisCluster cacheCluster, public AccountsManager(final Accounts accounts,
final PhoneNumberIdentifiers phoneNumberIdentifiers,
final 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,
final UsernamesManager usernamesManager, final UsernamesManager usernamesManager,
final ProfilesManager profilesManager, final ProfilesManager profilesManager,
final StoredVerificationCodeManager pendingAccounts, final StoredVerificationCodeManager pendingAccounts,
@ -101,6 +106,7 @@ public class AccountsManager {
final ClientPresenceManager clientPresenceManager, final ClientPresenceManager clientPresenceManager,
final Clock clock) { final Clock clock) {
this.accounts = accounts; this.accounts = accounts;
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
this.cacheCluster = cacheCluster; this.cacheCluster = cacheCluster;
this.deletedAccountsManager = deletedAccountsManager; this.deletedAccountsManager = deletedAccountsManager;
this.directoryQueue = directoryQueue; this.directoryQueue = directoryQueue;
@ -137,7 +143,7 @@ public class AccountsManager {
device.setLastSeen(Util.todayInMillis()); device.setLastSeen(Util.todayInMillis());
device.setUserAgent(signalAgent); device.setUserAgent(signalAgent);
account.setNumber(number); account.setNumber(number, phoneNumberIdentifiers.getPhoneNumberIdentifier(number));
account.setUuid(maybeRecentlyDeletedUuid.orElseGet(UUID::randomUUID)); account.setUuid(maybeRecentlyDeletedUuid.orElseGet(UUID::randomUUID));
account.addDevice(device); account.addDevice(device);
account.setRegistrationLockFromAttributes(accountAttributes); account.setRegistrationLockFromAttributes(accountAttributes);
@ -148,7 +154,7 @@ public class AccountsManager {
final UUID originalUuid = account.getUuid(); final UUID originalUuid = account.getUuid();
boolean freshUser = dynamoCreate(account); boolean freshUser = accounts.create(account);
// create() sometimes updates the UUID, if there was a number conflict. // create() sometimes updates the UUID, if there was a number conflict.
// for metrics, we want secondary to run with the same original UUID // for metrics, we want secondary to run with the same original UUID
@ -210,7 +216,7 @@ public class AccountsManager {
deletedAccountsManager.lockAndPut(account.getNumber(), number, () -> { deletedAccountsManager.lockAndPut(account.getNumber(), number, () -> {
redisDelete(account); redisDelete(account);
final Optional<Account> maybeExistingAccount = get(number); final Optional<Account> maybeExistingAccount = getByE164(number);
final Optional<UUID> displacedUuid; final Optional<UUID> displacedUuid;
if (maybeExistingAccount.isPresent()) { if (maybeExistingAccount.isPresent()) {
@ -221,12 +227,13 @@ public class AccountsManager {
} }
final UUID uuid = account.getUuid(); final UUID uuid = account.getUuid();
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
final Account numberChangedAccount = updateWithRetries( final Account numberChangedAccount = updateWithRetries(
account, account,
a -> true, a -> true,
a -> dynamoChangeNumber(a, number), a -> accounts.changeNumber(a, number, phoneNumberIdentifier),
() -> dynamoGet(uuid).orElseThrow()); () -> accounts.getByAccountIdentifier(uuid).orElseThrow());
updatedAccount.set(numberChangedAccount); updatedAccount.set(numberChangedAccount);
directoryQueue.changePhoneNumber(numberChangedAccount, originalNumber, number); directoryQueue.changePhoneNumber(numberChangedAccount, originalNumber, number);
@ -286,7 +293,10 @@ public class AccountsManager {
final UUID uuid = account.getUuid(); final UUID uuid = account.getUuid();
final String originalNumber = account.getNumber(); final String originalNumber = account.getNumber();
updatedAccount = updateWithRetries(account, updater, this::dynamoUpdate, () -> dynamoGet(uuid).get()); updatedAccount = updateWithRetries(account,
updater,
accounts::update,
() -> accounts.getByAccountIdentifier(uuid).orElseThrow());
assert updatedAccount.getNumber().equals(originalNumber); assert updatedAccount.getNumber().equals(originalNumber);
@ -355,12 +365,12 @@ public class AccountsManager {
}); });
} }
public Optional<Account> get(String number) { public Optional<Account> getByE164(String number) {
try (Timer.Context ignored = getByNumberTimer.time()) { try (Timer.Context ignored = getByNumberTimer.time()) {
Optional<Account> account = redisGet(number); Optional<Account> account = redisGetByE164(number);
if (account.isEmpty()) { if (account.isEmpty()) {
account = dynamoGet(number); account = accounts.getByE164(number);
account.ifPresent(this::redisSet); account.ifPresent(this::redisSet);
} }
@ -368,12 +378,25 @@ public class AccountsManager {
} }
} }
public Optional<Account> get(UUID uuid) { public Optional<Account> getByPhoneNumberIdentifier(UUID pni) {
try (Timer.Context ignored = getByUuidTimer.time()) { try (Timer.Context ignored = getByNumberTimer.time()) {
Optional<Account> account = redisGet(uuid); Optional<Account> account = redisGetByPhoneNumberIdentifier(pni);
if (account.isEmpty()) { if (account.isEmpty()) {
account = dynamoGet(uuid); account = accounts.getByPhoneNumberIdentifier(pni);
account.ifPresent(this::redisSet);
}
return account;
}
}
public Optional<Account> getByAccountIdentifier(UUID uuid) {
try (Timer.Context ignored = getByUuidTimer.time()) {
Optional<Account> account = redisGetByAccountIdentifier(uuid);
if (account.isEmpty()) {
account = accounts.getByAccountIdentifier(uuid);
account.ifPresent(this::redisSet); account.ifPresent(this::redisSet);
} }
@ -417,19 +440,24 @@ public class AccountsManager {
keysDynamoDb.delete(account.getUuid()); keysDynamoDb.delete(account.getUuid());
messagesManager.clear(account.getUuid()); messagesManager.clear(account.getUuid());
account.getPhoneNumberIdentifier().ifPresent(pni -> {
keysDynamoDb.delete(pni);
messagesManager.clear(pni);
});
deleteStorageServiceDataFuture.join(); deleteStorageServiceDataFuture.join();
deleteBackupServiceDataFuture.join(); deleteBackupServiceDataFuture.join();
redisDelete(account); redisDelete(account);
dynamoDelete(account); accounts.delete(account.getUuid());
RedisOperation.unchecked(() -> RedisOperation.unchecked(() ->
account.getDevices().forEach(device -> account.getDevices().forEach(device ->
clientPresenceManager.displacePresence(account.getUuid(), device.getId()))); clientPresenceManager.displacePresence(account.getUuid(), device.getId())));
} }
private String getAccountMapKey(String number) { private String getAccountMapKey(String key) {
return "AccountMap::" + number; return "AccountMap::" + key;
} }
private String getAccountEntityKey(UUID uuid) { private String getAccountEntityKey(UUID uuid) {
@ -443,6 +471,9 @@ public class AccountsManager {
cacheCluster.useCluster(connection -> { cacheCluster.useCluster(connection -> {
final RedisAdvancedClusterCommands<String, String> commands = connection.sync(); final RedisAdvancedClusterCommands<String, String> commands = connection.sync();
account.getPhoneNumberIdentifier().ifPresent(pni ->
commands.set(getAccountMapKey(pni.toString()), account.getUuid().toString()));
commands.set(getAccountMapKey(account.getNumber()), account.getUuid().toString()); commands.set(getAccountMapKey(account.getNumber()), account.getUuid().toString());
commands.set(getAccountEntityKey(account.getUuid()), accountJson); commands.set(getAccountEntityKey(account.getUuid()), accountJson);
}); });
@ -451,11 +482,19 @@ public class AccountsManager {
} }
} }
private Optional<Account> redisGet(String number) { private Optional<Account> redisGetByPhoneNumberIdentifier(UUID uuid) {
try (Timer.Context ignored = redisNumberGetTimer.time()) { return redisGetBySecondaryKey(uuid.toString(), redisPniGetTimer);
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(number))); }
if (uuid != null) return redisGet(UUID.fromString(uuid)); private Optional<Account> redisGetByE164(String e164) {
return redisGetBySecondaryKey(e164, redisNumberGetTimer);
}
private Optional<Account> redisGetBySecondaryKey(String secondaryKey, Timer timer) {
try (Timer.Context ignored = timer.time()) {
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(getAccountMapKey(secondaryKey)));
if (uuid != null) return redisGetByAccountIdentifier(UUID.fromString(uuid));
else return Optional.empty(); else return Optional.empty();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
logger.warn("Deserialization error", e); logger.warn("Deserialization error", e);
@ -466,7 +505,7 @@ public class AccountsManager {
} }
} }
private Optional<Account> redisGet(UUID uuid) { private Optional<Account> redisGetByAccountIdentifier(UUID uuid) {
try (Timer.Context ignored = redisUuidGetTimer.time()) { try (Timer.Context ignored = redisUuidGetTimer.time()) {
final String json = cacheCluster.withCluster(connection -> connection.sync().get(getAccountEntityKey(uuid))); final String json = cacheCluster.withCluster(connection -> connection.sync().get(getAccountEntityKey(uuid)));
@ -489,32 +528,11 @@ public class AccountsManager {
private void redisDelete(final Account account) { private void redisDelete(final Account account) {
try (final Timer.Context ignored = redisDeleteTimer.time()) { try (final Timer.Context ignored = redisDeleteTimer.time()) {
cacheCluster.useCluster(connection -> connection.sync() cacheCluster.useCluster(connection -> {
.del(getAccountMapKey(account.getNumber()), getAccountEntityKey(account.getUuid()))); connection.sync().del(getAccountMapKey(account.getNumber()), getAccountEntityKey(account.getUuid()));
account.getPhoneNumberIdentifier().ifPresent(pni -> connection.sync().del(getAccountMapKey(pni.toString())));
});
} }
} }
private Optional<Account> dynamoGet(String number) {
return accounts.get(number);
}
private Optional<Account> dynamoGet(UUID uuid) {
return accounts.get(uuid);
}
private boolean dynamoCreate(Account account) {
return accounts.create(account);
}
private void dynamoUpdate(Account account) {
accounts.update(account);
}
private void dynamoDelete(final Account account) {
accounts.delete(account.getUuid());
}
private void dynamoChangeNumber(final Account account, final String number) {
accounts.changeNumber(account, number);
}
} }

View File

@ -43,7 +43,7 @@ public class ContactDiscoveryWriter extends AccountDatabaseCrawlerListener {
// Its less than ideal, but crawler listeners currently must not call update() // Its less than ideal, but crawler listeners currently must not call update()
// with the accounts from the chunk, because updates cause the account instance to become stale. Instead, they // with the accounts from the chunk, because updates cause the account instance to become stale. Instead, they
// must get a new copy, which they are free to update. // must get a new copy, which they are free to update.
accounts.get(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER)); accounts.getByAccountIdentifier(account.getUuid()).ifPresent(a -> accounts.update(a, NOOP_UPDATER));
} }
} }
} }

View File

@ -146,7 +146,7 @@ public class MessagePersister implements Managed {
@VisibleForTesting @VisibleForTesting
void persistQueue(final UUID accountUuid, final long deviceId) { void persistQueue(final UUID accountUuid, final long deviceId) {
final Optional<Account> maybeAccount = accountsManager.get(accountUuid); final Optional<Account> maybeAccount = accountsManager.getByAccountIdentifier(accountUuid);
if (maybeAccount.isEmpty()) { if (maybeAccount.isEmpty()) {
logger.error("No account record found for account {}", accountUuid); logger.error("No account record found for account {}", accountUuid);

View File

@ -62,7 +62,7 @@ public class NonNormalizedAccountCrawlerListener extends AccountDatabaseCrawlerL
workingNonNormalizedNumbers++; workingNonNormalizedNumbers++;
try { try {
final Optional<Account> maybeConflictingAccount = accountsManager.get(getNormalizedNumber(account)); final Optional<Account> maybeConflictingAccount = accountsManager.getByE164(getNormalizedNumber(account));
if (maybeConflictingAccount.isPresent()) { if (maybeConflictingAccount.isPresent()) {
workingConflictingNumbers++; workingConflictingNumbers++;

View File

@ -0,0 +1,85 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;
import java.util.Map;
import java.util.UUID;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
/**
* Manages a global, persistent mapping of phone numbers to phone number identifiers regardless of whether those
* numbers/identifiers are actually associated with an account.
*/
public class PhoneNumberIdentifiers {
private final DynamoDbClient dynamoDbClient;
private final String tableName;
@VisibleForTesting
static final String KEY_E164 = "P";
private static final String ATTR_PHONE_NUMBER_IDENTIFIER = "PNI";
private static final Timer GET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "get"));
private static final Timer SET_PNI_TIMER = Metrics.timer(name(PhoneNumberIdentifiers.class, "set"));
public PhoneNumberIdentifiers(final DynamoDbClient dynamoDbClient, final String tableName) {
this.dynamoDbClient = dynamoDbClient;
this.tableName = tableName;
}
/**
* Returns the phone number identifier (PNI) associated with the given phone number.
*
* @param phoneNumber the phone number for which to retrieve a phone number identifier
* @return the phone number identifier associated with the given phone number
*/
public UUID getPhoneNumberIdentifier(final String phoneNumber) {
final GetItemResponse response = GET_PNI_TIMER.record(() -> dynamoDbClient.getItem(GetItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
.projectionExpression(ATTR_PHONE_NUMBER_IDENTIFIER)
.build()));
final UUID phoneNumberIdentifier;
if (response.hasItem()) {
phoneNumberIdentifier = AttributeValues.getUUID(response.item(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
} else {
phoneNumberIdentifier = generatePhoneNumberIdentifierIfNotExists(phoneNumber);
}
if (phoneNumberIdentifier == null) {
throw new RuntimeException("Could not retrieve phone number identifier from stored item");
}
return phoneNumberIdentifier;
}
@VisibleForTesting
UUID generatePhoneNumberIdentifierIfNotExists(final String phoneNumber) {
final UpdateItemResponse response = SET_PNI_TIMER.record(() -> dynamoDbClient.updateItem(UpdateItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_E164, AttributeValues.fromString(phoneNumber)))
.updateExpression("SET #pni = if_not_exists(#pni, :pni)")
.expressionAttributeNames(Map.of("#pni", ATTR_PHONE_NUMBER_IDENTIFIER))
.expressionAttributeValues(Map.of(":pni", AttributeValues.fromUUID(UUID.randomUUID())))
.returnValues(ReturnValue.ALL_NEW)
.build()));
return AttributeValues.getUUID(response.attributes(), ATTR_PHONE_NUMBER_IDENTIFIER, null);
}
}

View File

@ -58,7 +58,7 @@ public class PushFeedbackProcessor extends AccountDatabaseCrawlerListener {
if (update) { if (update) {
// fetch a new version, since the chunk is shared and implicitly read-only // fetch a new version, since the chunk is shared and implicitly read-only
accountsManager.get(account.getUuid()).ifPresent(accountToUpdate -> { accountsManager.getByAccountIdentifier(account.getUuid()).ifPresent(accountToUpdate -> {
accountsManager.update(accountToUpdate, a -> { accountsManager.update(accountToUpdate, a -> {
for (Device device : a.getDevices()) { for (Device device : a.getDevices()) {
if (deviceNeedsUpdate(device)) { if (deviceNeedsUpdate(device)) {

View File

@ -24,7 +24,7 @@ public class RefreshingAccountAndDeviceSupplier implements Supplier<Pair<Account
@Override @Override
public Pair<Account, Device> get() { public Pair<Account, Device> get() {
if (account.isStale()) { if (account.isStale()) {
account = accountsManager.get(account.getUuid()) account = accountsManager.getByAccountIdentifier(account.getUuid())
.orElseThrow(() -> new RuntimeException("Could not find account")); .orElseThrow(() -> new RuntimeException("Could not find account"));
device = account.getDevice(device.getId()) device = account.getDevice(device.getId())
.orElseThrow(() -> new RefreshingAccountAndDeviceNotFoundException("Could not find device")); .orElseThrow(() -> new RefreshingAccountAndDeviceNotFoundException("Could not find device"));

View File

@ -47,7 +47,7 @@ public class DeadLetterHandler implements DispatchChannel {
switch (pubSubMessage.getType().getNumber()) { switch (pubSubMessage.getType().getNumber()) {
case PubSubMessage.Type.DELIVER_VALUE: case PubSubMessage.Type.DELIVER_VALUE:
Envelope message = Envelope.parseFrom(pubSubMessage.getContent()); Envelope message = Envelope.parseFrom(pubSubMessage.getContent());
Optional<Account> maybeAccount = accountsManager.get(address.getNumber()); Optional<Account> maybeAccount = accountsManager.getByE164(address.getNumber());
if (maybeAccount.isPresent()) { if (maybeAccount.isPresent()) {
messagesManager.insert(maybeAccount.get().getUuid(), address.getDeviceId(), message); messagesManager.insert(maybeAccount.get().getUuid(), address.getDeviceId(), message);

View File

@ -48,6 +48,7 @@ import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesCache; import org.whispersystems.textsecuregcm.storage.MessagesCache;
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb; import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.Profiles; import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb; import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
@ -112,6 +113,9 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client( DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig.client(
configuration.getDeletedAccountsDynamoDbConfiguration(), configuration.getDeletedAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create()); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
DynamoDbFromConfig.client(configuration.getPhoneNumberIdentifiersDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster",
configuration.getCacheClusterConfiguration(), redisClusterClientResources); configuration.getCacheClusterConfiguration(), redisClusterClientResources);
@ -156,7 +160,10 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
Accounts accounts = new Accounts(accountsDynamoDbClient, Accounts accounts = new Accounts(accountsDynamoDbClient,
configuration.getAccountsDynamoDbConfiguration().getTableName(), configuration.getAccountsDynamoDbConfiguration().getTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(), configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
configuration.getAccountsDynamoDbConfiguration().getScanPageSize()); configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
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);
@ -199,12 +206,12 @@ 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(accounts, cacheCluster, AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock); pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
for (String user : users) { for (String user : users) {
Optional<Account> account = accountsManager.get(user); Optional<Account> account = accountsManager.getByE164(user);
if (account.isPresent()) { if (account.isPresent()) {
accountsManager.delete(account.get(), DeletionReason.ADMIN_DELETED); accountsManager.delete(account.get(), DeletionReason.ADMIN_DELETED);

View File

@ -46,6 +46,7 @@ import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesCache; import org.whispersystems.textsecuregcm.storage.MessagesCache;
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb; import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.Profiles; import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb; import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
@ -114,6 +115,9 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig DynamoDbClient deletedAccountsDynamoDbClient = DynamoDbFromConfig
.client(configuration.getDeletedAccountsDynamoDbConfiguration(), .client(configuration.getDeletedAccountsDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create()); software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
DynamoDbClient phoneNumberIdentifiersDynamoDbClient =
DynamoDbFromConfig.client(configuration.getPhoneNumberIdentifiersDynamoDbConfiguration(),
software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider.create());
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster",
configuration.getCacheClusterConfiguration(), redisClusterClientResources); configuration.getCacheClusterConfiguration(), redisClusterClientResources);
@ -161,7 +165,10 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
Accounts accounts = new Accounts(accountsDynamoDbClient, Accounts accounts = new Accounts(accountsDynamoDbClient,
configuration.getAccountsDynamoDbConfiguration().getTableName(), configuration.getAccountsDynamoDbConfiguration().getTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(), configuration.getAccountsDynamoDbConfiguration().getPhoneNumberTableName(),
configuration.getAccountsDynamoDbConfiguration().getPhoneNumberIdentifierTableName(),
configuration.getAccountsDynamoDbConfiguration().getScanPageSize()); configuration.getAccountsDynamoDbConfiguration().getScanPageSize());
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(phoneNumberIdentifiersDynamoDbClient,
configuration.getPhoneNumberIdentifiersDynamoDbConfiguration().getTableName());
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);
@ -202,16 +209,16 @@ 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(accounts, cacheCluster, AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager, deletedAccountsManager, directoryQueue, keysDynamoDb, messagesManager, usernamesManager, profilesManager,
pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock); pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager, clock);
Optional<Account> maybeAccount; Optional<Account> maybeAccount;
try { try {
maybeAccount = accountsManager.get(UUID.fromString(namespace.getString("user"))); maybeAccount = accountsManager.getByAccountIdentifier(UUID.fromString(namespace.getString("user")));
} catch (final IllegalArgumentException e) { } catch (final IllegalArgumentException e) {
maybeAccount = accountsManager.get(namespace.getString("user")); maybeAccount = accountsManager.getByE164(namespace.getString("user"));
} }
maybeAccount.ifPresentOrElse(account -> { maybeAccount.ifPresentOrElse(account -> {

View File

@ -128,7 +128,7 @@ class AuthEnablementRefreshRequirementProviderTest {
account.addDevice(authenticatedDevice); account.addDevice(authenticatedDevice);
LongStream.range(2, 4).forEach(deviceId -> account.addDevice(DevicesHelper.createDevice(deviceId))); LongStream.range(2, 4).forEach(deviceId -> account.addDevice(DevicesHelper.createDevice(deviceId)));
when(accountsManager.get(uuid)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
account.getDevices() account.getDevices()
.forEach(device -> when(clientPresenceManager.isPresent(uuid, device.getId())).thenReturn(true)); .forEach(device -> when(clientPresenceManager.isPresent(uuid, device.getId())).thenReturn(true));
@ -310,7 +310,7 @@ class AuthEnablementRefreshRequirementProviderTest {
assertEquals(200, response.getStatus()); assertEquals(200, response.getStatus());
verify(accountsManager, never()).get(any(UUID.class)); verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class));
} }
@Nested @Nested

View File

@ -31,9 +31,6 @@ 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.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
@ -62,11 +59,14 @@ class BaseAccountAuthenticatorTest {
clock = mock(Clock.class); clock = mock(Clock.class);
baseAccountAuthenticator = new BaseAccountAuthenticator(accountsManager, clock); baseAccountAuthenticator = new BaseAccountAuthenticator(accountsManager, clock);
acct1 = new Account("+14088675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null, acct1 = new Account("+14088675309", AuthHelper.getRandomUUID(random), UUID.randomUUID(),
Set.of(new Device(1, null, null, null,
null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null); null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null);
acct2 = new Account("+14098675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null, acct2 = new Account("+14098675309", AuthHelper.getRandomUUID(random), UUID.randomUUID(),
Set.of(new Device(1, null, null, null,
null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null); null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null);
oldAccount = new Account("+14108675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null, oldAccount = new Account("+14108675309", AuthHelper.getRandomUUID(random), UUID.randomUUID(),
Set.of(new Device(1, null, null, null,
null, null, null, false, 0, null, oldTime, 0, null, 0, null)), null); null, null, null, false, 0, null, oldTime, 0, null, 0, null)), null);
AccountsHelper.setupMockUpdate(accountsManager); AccountsHelper.setupMockUpdate(accountsManager);
@ -156,7 +156,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now()); when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid); when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true); when(account.isEnabled()).thenReturn(true);
@ -184,7 +184,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now()); when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid); when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true); when(account.isEnabled()).thenReturn(true);
@ -213,7 +213,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now()); when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid); when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(false); when(account.isEnabled()).thenReturn(false);
@ -251,7 +251,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now()); when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid); when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true); when(account.isEnabled()).thenReturn(true);
@ -278,7 +278,7 @@ class BaseAccountAuthenticatorTest {
final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); final AuthenticationCredentials credentials = mock(AuthenticationCredentials.class);
when(clock.instant()).thenReturn(Instant.now()); when(clock.instant()).thenReturn(Instant.now());
when(accountsManager.get(uuid)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
when(account.getUuid()).thenReturn(uuid); when(account.getUuid()).thenReturn(uuid);
when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); when(account.getDevice(deviceId)).thenReturn(Optional.of(device));
when(account.isEnabled()).thenReturn(true); when(account.isEnabled()).thenReturn(true);
@ -303,7 +303,7 @@ class BaseAccountAuthenticatorTest {
() -> baseAccountAuthenticator.authenticate(new BasicCredentials(username, "password"), true)); () -> baseAccountAuthenticator.authenticate(new BasicCredentials(username, "password"), true));
assertThat(maybeAuthenticatedAccount).isEmpty(); assertThat(maybeAuthenticatedAccount).isEmpty();
verify(accountsManager, never()).get(any(UUID.class)); verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class));
} }
private static Stream<String> testAuthenticateMalformedCredentials() { private static Stream<String> testAuthenticateMalformedCredentials() {

View File

@ -57,8 +57,8 @@ class ApnFallbackManagerTest {
when(account.getDevice(DEVICE_ID)).thenReturn(Optional.of(device)); when(account.getDevice(DEVICE_ID)).thenReturn(Optional.of(device));
final AccountsManager accountsManager = mock(AccountsManager.class); final AccountsManager accountsManager = mock(AccountsManager.class);
when(accountsManager.get(ACCOUNT_NUMBER)).thenReturn(Optional.of(account)); when(accountsManager.getByE164(ACCOUNT_NUMBER)).thenReturn(Optional.of(account));
when(accountsManager.get(ACCOUNT_UUID)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(ACCOUNT_UUID)).thenReturn(Optional.of(account));
apnSender = mock(APNSender.class); apnSender = mock(APNSender.class);

View File

@ -6,6 +6,7 @@
package org.whispersystems.textsecuregcm.storage; package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
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.Mockito.mock; import static org.mockito.Mockito.mock;
@ -49,6 +50,8 @@ class AccountsManagerChangeNumberIntegrationTest {
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 String PNI_ASSIGNMENT_TABLE_NAME = "pni_assignment_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 NEEDS_RECONCILIATION_INDEX_NAME = "needs_reconciliation_test";
private static final String DELETED_ACCOUNTS_LOCK_TABLE_NAME = "deleted_accounts_lock_test"; private static final String DELETED_ACCOUNTS_LOCK_TABLE_NAME = "deleted_accounts_lock_test";
private static final int SCAN_PAGE_SIZE = 1; private static final int SCAN_PAGE_SIZE = 1;
@ -92,6 +95,16 @@ class AccountsManagerChangeNumberIntegrationTest {
.attributeType(ScalarAttributeType.S).build()) .attributeType(ScalarAttributeType.S).build())
.build(); .build();
@RegisterExtension
static DynamoDbExtension PNI_DYNAMO_EXTENSION = DynamoDbExtension.builder()
.tableName(PNI_TABLE_NAME)
.hashKey(PhoneNumberIdentifiers.KEY_E164)
.attributeDefinition(AttributeDefinition.builder()
.attributeName(PhoneNumberIdentifiers.KEY_E164)
.attributeType(ScalarAttributeType.S)
.build())
.build();
@RegisterExtension @RegisterExtension
static RedisClusterExtension CACHE_CLUSTER_EXTENSION = RedisClusterExtension.builder().build(); static RedisClusterExtension CACHE_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
@ -120,14 +133,33 @@ class AccountsManagerChangeNumberIntegrationTest {
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().createTable(createNumbersTableRequest); ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().createTable(createNumbersTableRequest);
} }
{
CreateTableRequest createPhoneNumberIdentifierTableRequest = CreateTableRequest.builder()
.tableName(PNI_ASSIGNMENT_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.attributeType(ScalarAttributeType.B)
.build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
.build();
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
}
final Accounts accounts = new Accounts( final Accounts accounts = new Accounts(
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient(), ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient(),
ACCOUNTS_DYNAMO_EXTENSION.getTableName(), ACCOUNTS_DYNAMO_EXTENSION.getTableName(),
NUMBERS_TABLE_NAME, NUMBERS_TABLE_NAME,
PNI_ASSIGNMENT_TABLE_NAME,
SCAN_PAGE_SIZE); SCAN_PAGE_SIZE);
{ {
final DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); @SuppressWarnings("unchecked") final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
mock(DynamicConfigurationManager.class);
DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(); DynamicConfiguration dynamicConfiguration = new DynamicConfiguration();
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
@ -148,8 +180,12 @@ class AccountsManagerChangeNumberIntegrationTest {
clientPresenceManager = mock(ClientPresenceManager.class); clientPresenceManager = mock(ClientPresenceManager.class);
final PhoneNumberIdentifiers phoneNumberIdentifiers =
new PhoneNumberIdentifiers(PNI_DYNAMO_EXTENSION.getDynamoDbClient(), PNI_TABLE_NAME);
accountsManager = new AccountsManager( accountsManager = new AccountsManager(
accounts, accounts,
phoneNumberIdentifiers,
CACHE_CLUSTER_EXTENSION.getRedisCluster(), CACHE_CLUSTER_EXTENSION.getRedisCluster(),
deletedAccountsManager, deletedAccountsManager,
mock(DirectoryQueue.class), mock(DirectoryQueue.class),
@ -172,15 +208,17 @@ class AccountsManagerChangeNumberIntegrationTest {
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>()); final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID originalUuid = account.getUuid(); final UUID originalUuid = account.getUuid();
final UUID originalPni = account.getPhoneNumberIdentifier().orElseThrow();
accountsManager.changeNumber(account, secondNumber); accountsManager.changeNumber(account, secondNumber);
assertTrue(accountsManager.get(originalNumber).isEmpty()); assertTrue(accountsManager.getByE164(originalNumber).isEmpty());
assertTrue(accountsManager.get(secondNumber).isPresent()); assertTrue(accountsManager.getByE164(secondNumber).isPresent());
assertEquals(Optional.of(originalUuid), accountsManager.get(secondNumber).map(Account::getUuid)); assertEquals(originalUuid, accountsManager.getByE164(secondNumber).map(Account::getUuid).orElseThrow());
assertNotEquals(originalPni, accountsManager.getByE164(secondNumber).flatMap(Account::getPhoneNumberIdentifier).orElseThrow());
assertEquals(secondNumber, accountsManager.get(originalUuid).map(Account::getNumber).orElseThrow()); assertEquals(secondNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow());
assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber)); assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber));
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber)); assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
@ -193,16 +231,18 @@ class AccountsManagerChangeNumberIntegrationTest {
Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>()); Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID originalUuid = account.getUuid(); final UUID originalUuid = account.getUuid();
final UUID originalPni = account.getPhoneNumberIdentifier().orElseThrow();
account = accountsManager.changeNumber(account, secondNumber); account = accountsManager.changeNumber(account, secondNumber);
accountsManager.changeNumber(account, originalNumber); accountsManager.changeNumber(account, originalNumber);
assertTrue(accountsManager.get(originalNumber).isPresent()); assertTrue(accountsManager.getByE164(originalNumber).isPresent());
assertEquals(Optional.of(originalUuid), accountsManager.get(originalNumber).map(Account::getUuid)); assertEquals(originalUuid, accountsManager.getByE164(originalNumber).map(Account::getUuid).orElseThrow());
assertEquals(originalPni, accountsManager.getByE164(originalNumber).flatMap(Account::getPhoneNumberIdentifier).orElseThrow());
assertTrue(accountsManager.get(secondNumber).isEmpty()); assertTrue(accountsManager.getByE164(secondNumber).isEmpty());
assertEquals(originalNumber, accountsManager.get(originalUuid).map(Account::getNumber).orElseThrow()); assertEquals(originalNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow());
assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber)); assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber));
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber)); assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
@ -223,12 +263,12 @@ class AccountsManagerChangeNumberIntegrationTest {
accountsManager.changeNumber(account, secondNumber); accountsManager.changeNumber(account, secondNumber);
assertTrue(accountsManager.get(originalNumber).isEmpty()); assertTrue(accountsManager.getByE164(originalNumber).isEmpty());
assertTrue(accountsManager.get(secondNumber).isPresent()); assertTrue(accountsManager.getByE164(secondNumber).isPresent());
assertEquals(Optional.of(originalUuid), accountsManager.get(secondNumber).map(Account::getUuid)); assertEquals(Optional.of(originalUuid), accountsManager.getByE164(secondNumber).map(Account::getUuid));
assertEquals(secondNumber, accountsManager.get(originalUuid).map(Account::getNumber).orElseThrow()); assertEquals(secondNumber, accountsManager.getByAccountIdentifier(originalUuid).map(Account::getNumber).orElseThrow());
verify(clientPresenceManager).displacePresence(existingAccountUuid, Device.MASTER_ID); verify(clientPresenceManager).displacePresence(existingAccountUuid, Device.MASTER_ID);
@ -243,22 +283,26 @@ class AccountsManagerChangeNumberIntegrationTest {
final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>()); final Account account = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID originalUuid = account.getUuid(); final UUID originalUuid = account.getUuid();
final UUID originalPni = account.getPhoneNumberIdentifier().orElseThrow();
final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes(), new ArrayList<>()); final Account existingAccount = accountsManager.create(secondNumber, "password", null, new AccountAttributes(), new ArrayList<>());
final UUID existingAccountUuid = existingAccount.getUuid(); final UUID existingAccountUuid = existingAccount.getUuid();
accountsManager.changeNumber(account, secondNumber); final Account changedNumberAccount = accountsManager.changeNumber(account, secondNumber);
final UUID secondPni = changedNumberAccount.getPhoneNumberIdentifier().orElseThrow();
final Account reRegisteredAccount = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>()); final Account reRegisteredAccount = accountsManager.create(originalNumber, "password", null, new AccountAttributes(), new ArrayList<>());
assertEquals(existingAccountUuid, reRegisteredAccount.getUuid()); assertEquals(existingAccountUuid, reRegisteredAccount.getUuid());
assertEquals(originalPni, reRegisteredAccount.getPhoneNumberIdentifier().orElseThrow());
assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber)); assertEquals(Optional.empty(), deletedAccounts.findUuid(originalNumber));
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber)); assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
accountsManager.changeNumber(reRegisteredAccount, secondNumber); final Account changedNumberReRegisteredAccount = accountsManager.changeNumber(reRegisteredAccount, secondNumber);
assertEquals(Optional.of(originalUuid), deletedAccounts.findUuid(originalNumber)); assertEquals(Optional.of(originalUuid), deletedAccounts.findUuid(originalNumber));
assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber)); assertEquals(Optional.empty(), deletedAccounts.findUuid(secondNumber));
assertEquals(secondPni, changedNumberReRegisteredAccount.getPhoneNumberIdentifier().orElseThrow());
} }
} }

View File

@ -15,6 +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 io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import java.io.IOException; import java.io.IOException;
@ -35,6 +36,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.SignedPreKey; import org.whispersystems.textsecuregcm.entities.SignedPreKey;
@ -55,6 +58,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
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 String PNI_TABLE_NAME = "pni_test";
private static final int SCAN_PAGE_SIZE = 1; private static final int SCAN_PAGE_SIZE = 1;
@ -96,10 +100,28 @@ class AccountsManagerConcurrentModificationIntegrationTest {
dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest); dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest);
} }
{
CreateTableRequest createPhoneNumberIdentifierTableRequest = CreateTableRequest.builder()
.tableName(PNI_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.attributeType(ScalarAttributeType.B)
.build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
.build();
dynamoDbExtension.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
}
accounts = new Accounts( accounts = new Accounts(
dynamoDbExtension.getDynamoDbClient(), dynamoDbExtension.getDynamoDbClient(),
dynamoDbExtension.getTableName(), dynamoDbExtension.getTableName(),
NUMBERS_TABLE_NAME, NUMBERS_TABLE_NAME,
PNI_TABLE_NAME,
SCAN_PAGE_SIZE); SCAN_PAGE_SIZE);
{ {
@ -114,8 +136,13 @@ class AccountsManagerConcurrentModificationIntegrationTest {
return null; return null;
}).when(deletedAccountsManager).lockAndTake(anyString(), any()); }).when(deletedAccountsManager).lockAndTake(anyString(), any());
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString()))
.thenAnswer((Answer<UUID>) invocation -> UUID.randomUUID());
accountsManager = new AccountsManager( accountsManager = new AccountsManager(
accounts, accounts,
phoneNumberIdentifiers,
RedisClusterHelper.buildMockRedisCluster(commands), RedisClusterHelper.buildMockRedisCluster(commands),
deletedAccountsManager, deletedAccountsManager,
mock(DirectoryQueue.class), mock(DirectoryQueue.class),
@ -185,8 +212,8 @@ class AccountsManagerConcurrentModificationIntegrationTest {
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).orElseThrow(); final Account managerAccount = accountsManager.getByAccountIdentifier(uuid).orElseThrow();
final Account dynamoAccount = accounts.get(uuid).orElseThrow(); final Account dynamoAccount = accounts.getByAccountIdentifier(uuid).orElseThrow();
final Account redisAccount = getLastAccountFromRedisMock(commands); final Account redisAccount = getLastAccountFromRedisMock(commands);
@ -225,7 +252,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).orElseThrow(); final Account account = accountsManager.getByAccountIdentifier(uuid).orElseThrow();
accountsManager.update(account, accountMutation); accountsManager.update(account, accountMutation);
}, mutationExecutor); }, mutationExecutor);
} }
@ -233,7 +260,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).orElseThrow(); final Account account = accountsManager.getByAccountIdentifier(uuid).orElseThrow();
accountsManager.updateDevice(account, deviceId, deviceMutation); accountsManager.updateDevice(account, deviceId, deviceMutation);
}, mutationExecutor); }, mutationExecutor);
} }

View File

@ -12,12 +12,14 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.uuid.UUIDComparator; import com.fasterxml.uuid.UUIDComparator;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException; import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -25,11 +27,6 @@ import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.AssertionsForClassTypes;
import org.jdbi.v3.core.transaction.TransactionException; import org.jdbi.v3.core.transaction.TransactionException;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
@ -38,15 +35,20 @@ import org.junit.jupiter.api.extension.RegisterExtension;
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
import org.whispersystems.textsecuregcm.entities.SignedPreKey; import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.AttributeValues; 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.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
import software.amazon.awssdk.services.dynamodb.model.KeyType; import software.amazon.awssdk.services.dynamodb.model.KeyType;
import software.amazon.awssdk.services.dynamodb.model.Put;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest; import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException; import software.amazon.awssdk.services.dynamodb.model.TransactionConflictException;
@ -55,7 +57,8 @@ import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
class AccountsTest { 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 NUMBER_CONSTRAINT_TABLE_NAME = "numbers_test";
private static final String PNI_CONSTRAINT_TABLE_NAME = "pni_test";
private static final int SCAN_PAGE_SIZE = 1; private static final int SCAN_PAGE_SIZE = 1;
@ -74,7 +77,7 @@ class AccountsTest {
@BeforeEach @BeforeEach
void setupAccountsDao() { void setupAccountsDao() {
CreateTableRequest createNumbersTableRequest = CreateTableRequest.builder() CreateTableRequest createNumbersTableRequest = CreateTableRequest.builder()
.tableName(NUMBERS_TABLE_NAME) .tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.keySchema(KeySchemaElement.builder() .keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_ACCOUNT_E164) .attributeName(Accounts.ATTR_ACCOUNT_E164)
.keyType(KeyType.HASH) .keyType(KeyType.HASH)
@ -88,27 +91,48 @@ class AccountsTest {
dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest); dynamoDbExtension.getDynamoDbClient().createTable(createNumbersTableRequest);
CreateTableRequest createPhoneNumberIdentifierTableRequest = CreateTableRequest.builder()
.tableName(PNI_CONSTRAINT_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(Accounts.ATTR_PNI_UUID)
.attributeType(ScalarAttributeType.B)
.build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
.build();
dynamoDbExtension.getDynamoDbClient().createTable(createPhoneNumberIdentifierTableRequest);
this.accounts = new Accounts( this.accounts = new Accounts(
dynamoDbExtension.getDynamoDbClient(), dynamoDbExtension.getDynamoDbClient(),
dynamoDbExtension.getTableName(), dynamoDbExtension.getTableName(),
NUMBERS_TABLE_NAME, NUMBER_CONSTRAINT_TABLE_NAME,
PNI_CONSTRAINT_TABLE_NAME,
SCAN_PAGE_SIZE); SCAN_PAGE_SIZE);
} }
@Test @Test
void testStore() { void testStore() {
Device device = generateDevice (1 ); Device device = generateDevice(1);
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device)); Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
boolean freshUser = accounts.create(account); boolean freshUser = accounts.create(account);
assertThat(freshUser).isTrue(); assertThat(freshUser).isTrue();
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, true);
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier().orElseThrow(), account.getUuid());
freshUser = accounts.create(account); freshUser = accounts.create(account);
assertThat(freshUser).isTrue(); assertThat(freshUser).isTrue();
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, true);
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier().orElseThrow(), account.getUuid());
} }
@Test @Test
@ -117,11 +141,14 @@ class AccountsTest {
devices.add(generateDevice(1)); devices.add(generateDevice(1));
devices.add(generateDevice(2)); devices.add(generateDevice(2));
Account account = generateAccount("+14151112222", UUID.randomUUID(), devices); Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), devices);
accounts.create(account); accounts.create(account);
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, true);
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier().orElseThrow(), account.getUuid());
} }
@Test @Test
@ -131,46 +158,123 @@ class AccountsTest {
devicesFirst.add(generateDevice(2)); devicesFirst.add(generateDevice(2));
UUID uuidFirst = UUID.randomUUID(); UUID uuidFirst = UUID.randomUUID();
Account accountFirst = generateAccount("+14151112222", uuidFirst, devicesFirst); UUID pniFirst = UUID.randomUUID();
Account accountFirst = generateAccount("+14151112222", uuidFirst, pniFirst, devicesFirst);
Set<Device> devicesSecond = new HashSet<>(); Set<Device> devicesSecond = new HashSet<>();
devicesSecond.add(generateDevice(1)); devicesSecond.add(generateDevice(1));
devicesSecond.add(generateDevice(2)); devicesSecond.add(generateDevice(2));
UUID uuidSecond = UUID.randomUUID(); UUID uuidSecond = UUID.randomUUID();
Account accountSecond = generateAccount("+14152221111", uuidSecond, devicesSecond); UUID pniSecond = UUID.randomUUID();
Account accountSecond = generateAccount("+14152221111", uuidSecond, pniSecond, devicesSecond);
accounts.create(accountFirst); accounts.create(accountFirst);
accounts.create(accountSecond); accounts.create(accountSecond);
Optional<Account> retrievedFirst = accounts.get("+14151112222"); Optional<Account> retrievedFirst = accounts.getByE164("+14151112222");
Optional<Account> retrievedSecond = accounts.get("+14152221111"); Optional<Account> retrievedSecond = accounts.getByE164("+14152221111");
assertThat(retrievedFirst.isPresent()).isTrue(); assertThat(retrievedFirst.isPresent()).isTrue();
assertThat(retrievedSecond.isPresent()).isTrue(); assertThat(retrievedSecond.isPresent()).isTrue();
verifyStoredState("+14151112222", uuidFirst, retrievedFirst.get(), accountFirst); verifyStoredState("+14151112222", uuidFirst, pniFirst, retrievedFirst.get(), accountFirst);
verifyStoredState("+14152221111", uuidSecond, retrievedSecond.get(), accountSecond); verifyStoredState("+14152221111", uuidSecond, pniSecond, retrievedSecond.get(), accountSecond);
retrievedFirst = accounts.get(uuidFirst); retrievedFirst = accounts.getByAccountIdentifier(uuidFirst);
retrievedSecond = accounts.get(uuidSecond); retrievedSecond = accounts.getByAccountIdentifier(uuidSecond);
assertThat(retrievedFirst.isPresent()).isTrue(); assertThat(retrievedFirst.isPresent()).isTrue();
assertThat(retrievedSecond.isPresent()).isTrue(); assertThat(retrievedSecond.isPresent()).isTrue();
verifyStoredState("+14151112222", uuidFirst, retrievedFirst.get(), accountFirst); verifyStoredState("+14151112222", uuidFirst, pniFirst, retrievedFirst.get(), accountFirst);
verifyStoredState("+14152221111", uuidSecond, retrievedSecond.get(), accountSecond); verifyStoredState("+14152221111", uuidSecond, pniSecond, retrievedSecond.get(), accountSecond);
retrievedFirst = accounts.getByPhoneNumberIdentifier(pniFirst);
retrievedSecond = accounts.getByPhoneNumberIdentifier(pniSecond);
assertThat(retrievedFirst.isPresent()).isTrue();
assertThat(retrievedSecond.isPresent()).isTrue();
verifyStoredState("+14151112222", uuidFirst, pniFirst, retrievedFirst.get(), accountFirst);
verifyStoredState("+14152221111", uuidSecond, pniSecond, retrievedSecond.get(), accountSecond);
}
@Test
void testRetrieveNoPni() throws JsonProcessingException {
final Set<Device> devices = new HashSet<>();
devices.add(generateDevice(1));
devices.add(generateDevice(2));
final UUID uuid = UUID.randomUUID();
final Account account = generateAccount("+14151112222", uuid, null, devices);
// Accounts#create enforces that newly-created accounts have a PNI, so we need to make a bit of an end-run around it
// to simulate an existing account with no PNI.
{
final TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.item(Map.of(
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID,
"#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
final TransactWriteItem accountPut = TransactWriteItem.builder()
.put(Put.builder()
.tableName(ACCOUNTS_TABLE_NAME)
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
.item(Map.of(
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid),
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
Accounts.ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
Accounts.ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())))
.build())
.build();
dynamoDbExtension.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
.transactItems(phoneNumberConstraintPut, accountPut)
.build());
}
Optional<Account> retrieved = accounts.getByE164("+14151112222");
assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", uuid, null, retrieved.get(), account);
retrieved = accounts.getByAccountIdentifier(uuid);
assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", uuid, null, retrieved.get(), account);
} }
@Test @Test
void testOverwrite() { void testOverwrite() {
Device device = generateDevice (1 ); Device device = generateDevice(1);
UUID firstUuid = UUID.randomUUID(); UUID firstUuid = UUID.randomUUID();
Account account = generateAccount("+14151112222", firstUuid, Collections.singleton(device)); UUID firstPni = UUID.randomUUID();
Account account = generateAccount("+14151112222", firstUuid, firstPni, Collections.singleton(device));
accounts.create(account); accounts.create(account);
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, true);
assertPhoneNumberConstraintExists("+14151112222", firstUuid);
assertPhoneNumberIdentifierConstraintExists(firstPni, firstUuid);
account.setProfileName("name"); account.setProfileName("name");
@ -179,14 +283,17 @@ class AccountsTest {
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, UUID.randomUUID(), Collections.singleton(device));
final boolean freshUser = accounts.create(account); final boolean freshUser = accounts.create(account);
assertThat(freshUser).isFalse(); assertThat(freshUser).isFalse();
verifyStoredState("+14151112222", firstUuid, account, true); verifyStoredState("+14151112222", firstUuid, firstPni, account, true);
assertPhoneNumberConstraintExists("+14151112222", firstUuid);
assertPhoneNumberIdentifierConstraintExists(firstPni, firstUuid);
device = generateDevice(1); device = generateDevice(1);
Account invalidAccount = generateAccount("+14151113333", firstUuid, Collections.singleton(device)); Account invalidAccount = generateAccount("+14151113333", firstUuid, UUID.randomUUID(), Collections.singleton(device));
assertThatThrownBy(() -> accounts.create(invalidAccount)); assertThatThrownBy(() -> accounts.create(invalidAccount));
} }
@ -194,28 +301,34 @@ class AccountsTest {
@Test @Test
void testUpdate() { void testUpdate() {
Device device = generateDevice (1 ); Device device = generateDevice (1 );
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device)); Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
accounts.create(account); accounts.create(account);
assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier().orElseThrow(), account.getUuid());
device.setName("foobar"); device.setName("foobar");
accounts.update(account); accounts.update(account);
Optional<Account> retrieved = accounts.get("+14151112222"); assertPhoneNumberConstraintExists("+14151112222", account.getUuid());
assertPhoneNumberIdentifierConstraintExists(account.getPhoneNumberIdentifier().orElseThrow(), account.getUuid());
Optional<Account> retrieved = accounts.getByE164("+14151112222");
assertThat(retrieved.isPresent()).isTrue(); assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", account.getUuid(), retrieved.get(), account); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), retrieved.get(), account);
retrieved = accounts.get(account.getUuid()); retrieved = accounts.getByAccountIdentifier(account.getUuid());
assertThat(retrieved.isPresent()).isTrue(); assertThat(retrieved.isPresent()).isTrue();
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, true);
device = generateDevice(1); device = generateDevice(1);
Account unknownAccount = generateAccount("+14151113333", UUID.randomUUID(), Collections.singleton(device)); Account unknownAccount = generateAccount("+14151113333", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
assertThatThrownBy(() -> accounts.update(unknownAccount)).isInstanceOfAny(ConditionalCheckFailedException.class); assertThatThrownBy(() -> accounts.update(unknownAccount)).isInstanceOfAny(TransactionCanceledException.class);
account.setProfileName("name"); account.setProfileName("name");
@ -223,7 +336,7 @@ class AccountsTest {
assertThat(account.getVersion()).isEqualTo(2); assertThat(account.getVersion()).isEqualTo(2);
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, true);
account.setVersion(1); account.setVersion(1);
@ -234,7 +347,7 @@ class AccountsTest {
accounts.update(account); accounts.update(account);
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, true);
} }
@Test @Test
@ -242,13 +355,13 @@ class AccountsTest {
final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class); final DynamoDbClient dynamoDbClient = mock(DynamoDbClient.class);
accounts = new Accounts(dynamoDbClient, accounts = new Accounts(dynamoDbClient,
dynamoDbExtension.getTableName(), NUMBERS_TABLE_NAME, SCAN_PAGE_SIZE); dynamoDbExtension.getTableName(), NUMBER_CONSTRAINT_TABLE_NAME, PNI_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
when(dynamoDbClient.updateItem(any(UpdateItemRequest.class))) when(dynamoDbClient.transactWriteItems(any(TransactWriteItemsRequest.class)))
.thenThrow(TransactionConflictException.class); .thenThrow(TransactionConflictException.class);
Device device = generateDevice(1); Device device = generateDevice(1);
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device)); Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
assertThatThrownBy(() -> accounts.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class); assertThatThrownBy(() -> accounts.update(account)).isInstanceOfAny(ContestedOptimisticLockException.class);
} }
@ -258,7 +371,7 @@ class AccountsTest {
List<Account> users = new ArrayList<>(); List<Account> users = new ArrayList<>();
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(), UUID.randomUUID());
users.add(account); users.add(account);
accounts.create(account); accounts.create(account);
} }
@ -276,7 +389,7 @@ class AccountsTest {
.findAny() .findAny()
.orElseThrow(); .orElseThrow();
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), retrievedAccount, expectedAccount); verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), expectedAccount.getPhoneNumberIdentifier().orElseThrow(), retrievedAccount, expectedAccount);
users.remove(expectedAccount); users.remove(expectedAccount);
} }
@ -293,7 +406,7 @@ class AccountsTest {
.findAny() .findAny()
.orElseThrow(); .orElseThrow();
verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), retrievedAccount, expectedAccount); verifyStoredState(expectedAccount.getNumber(), expectedAccount.getUuid(), expectedAccount.getPhoneNumberIdentifier().orElseThrow(), retrievedAccount, expectedAccount);
users.remove(expectedAccount); users.remove(expectedAccount);
} }
@ -306,48 +419,59 @@ class AccountsTest {
void testDelete() { void testDelete() {
final Device deletedDevice = generateDevice(1); final Device deletedDevice = generateDevice(1);
final Account deletedAccount = generateAccount("+14151112222", UUID.randomUUID(), final Account deletedAccount = generateAccount("+14151112222", UUID.randomUUID(),
Collections.singleton(deletedDevice)); UUID.randomUUID(), Collections.singleton(deletedDevice));
final Device retainedDevice = generateDevice(1); final Device retainedDevice = generateDevice(1);
final Account retainedAccount = generateAccount("+14151112345", UUID.randomUUID(), final Account retainedAccount = generateAccount("+14151112345", UUID.randomUUID(),
Collections.singleton(retainedDevice)); UUID.randomUUID(), Collections.singleton(retainedDevice));
accounts.create(deletedAccount); accounts.create(deletedAccount);
accounts.create(retainedAccount); accounts.create(retainedAccount);
assertThat(accounts.get(deletedAccount.getUuid())).isPresent(); assertPhoneNumberConstraintExists("+14151112222", deletedAccount.getUuid());
assertThat(accounts.get(retainedAccount.getUuid())).isPresent(); assertPhoneNumberIdentifierConstraintExists(deletedAccount.getPhoneNumberIdentifier().orElseThrow(), deletedAccount.getUuid());
assertPhoneNumberConstraintExists("+14151112345", retainedAccount.getUuid());
assertPhoneNumberIdentifierConstraintExists(retainedAccount.getPhoneNumberIdentifier().orElseThrow(), retainedAccount.getUuid());
assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isPresent();
assertThat(accounts.getByAccountIdentifier(retainedAccount.getUuid())).isPresent();
accounts.delete(deletedAccount.getUuid()); accounts.delete(deletedAccount.getUuid());
assertThat(accounts.get(deletedAccount.getUuid())).isNotPresent(); assertThat(accounts.getByAccountIdentifier(deletedAccount.getUuid())).isNotPresent();
verifyStoredState(retainedAccount.getNumber(), retainedAccount.getUuid(), assertPhoneNumberConstraintDoesNotExist(deletedAccount.getNumber());
accounts.get(retainedAccount.getUuid()).get(), retainedAccount); assertPhoneNumberIdentifierConstraintDoesNotExist(deletedAccount.getPhoneNumberIdentifier().orElseThrow());
verifyStoredState(retainedAccount.getNumber(), retainedAccount.getUuid(), retainedAccount.getPhoneNumberIdentifier().orElseThrow(),
accounts.getByAccountIdentifier(retainedAccount.getUuid()).get(), retainedAccount);
{ {
final Account recreatedAccount = generateAccount(deletedAccount.getNumber(), UUID.randomUUID(), final Account recreatedAccount = generateAccount(deletedAccount.getNumber(), UUID.randomUUID(),
Collections.singleton(generateDevice(1))); UUID.randomUUID(), Collections.singleton(generateDevice(1)));
final boolean freshUser = accounts.create(recreatedAccount); final boolean freshUser = accounts.create(recreatedAccount);
assertThat(freshUser).isTrue(); assertThat(freshUser).isTrue();
assertThat(accounts.get(recreatedAccount.getUuid())).isPresent(); assertThat(accounts.getByAccountIdentifier(recreatedAccount.getUuid())).isPresent();
verifyStoredState(recreatedAccount.getNumber(), recreatedAccount.getUuid(), verifyStoredState(recreatedAccount.getNumber(), recreatedAccount.getUuid(), recreatedAccount.getPhoneNumberIdentifier().orElseThrow(),
accounts.get(recreatedAccount.getUuid()).get(), recreatedAccount); accounts.getByAccountIdentifier(recreatedAccount.getUuid()).get(), recreatedAccount);
assertPhoneNumberConstraintExists(recreatedAccount.getNumber(), recreatedAccount.getUuid());
assertPhoneNumberIdentifierConstraintExists(recreatedAccount.getPhoneNumberIdentifier().orElseThrow(), recreatedAccount.getUuid());
} }
} }
@Test @Test
void testMissing() { void testMissing() {
Device device = generateDevice (1 ); Device device = generateDevice (1 );
Account account = generateAccount("+14151112222", UUID.randomUUID(), Collections.singleton(device)); Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
accounts.create(account); accounts.create(account);
Optional<Account> retrieved = accounts.get("+11111111"); Optional<Account> retrieved = accounts.getByE164("+11111111");
assertThat(retrieved.isPresent()).isFalse(); assertThat(retrieved.isPresent()).isFalse();
retrieved = accounts.get(UUID.randomUUID()); retrieved = accounts.getByAccountIdentifier(UUID.randomUUID());
assertThat(retrieved.isPresent()).isFalse(); assertThat(retrieved.isPresent()).isFalse();
} }
@ -369,8 +493,9 @@ class AccountsTest {
when(client.updateItem(any(UpdateItemRequest.class))) when(client.updateItem(any(UpdateItemRequest.class)))
.thenThrow(RuntimeException.class); .thenThrow(RuntimeException.class);
Accounts accounts = new Accounts(client, ACCOUNTS_TABLE_NAME, NUMBERS_TABLE_NAME, SCAN_PAGE_SIZE); Accounts accounts = new Accounts(client, ACCOUNTS_TABLE_NAME, NUMBER_CONSTRAINT_TABLE_NAME,
Account account = generateAccount("+14151112222", UUID.randomUUID()); PNI_CONSTRAINT_TABLE_NAME, SCAN_PAGE_SIZE);
Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID());
try { try {
accounts.update(account); accounts.update(account);
@ -406,17 +531,16 @@ class AccountsTest {
@Test @Test
void testCanonicallyDiscoverableSet() { void testCanonicallyDiscoverableSet() {
Device device = generateDevice(1); Device device = generateDevice(1);
UUID uuid = UUID.randomUUID(); Account account = generateAccount("+14151112222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
Account account = generateAccount("+14151112222", uuid, Collections.singleton(device));
account.setDiscoverableByPhoneNumber(false); account.setDiscoverableByPhoneNumber(false);
accounts.create(account); accounts.create(account);
verifyStoredState("+14151112222", account.getUuid(), account, false); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, false);
account.setDiscoverableByPhoneNumber(true); account.setDiscoverableByPhoneNumber(true);
accounts.update(account); accounts.update(account);
verifyStoredState("+14151112222", account.getUuid(), account, true); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, true);
account.setDiscoverableByPhoneNumber(false); account.setDiscoverableByPhoneNumber(false);
accounts.update(account); accounts.update(account);
verifyStoredState("+14151112222", account.getUuid(), account, false); verifyStoredState("+14151112222", account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), account, false);
} }
@Test @Test
@ -424,27 +548,45 @@ class AccountsTest {
final String originalNumber = "+14151112222"; final String originalNumber = "+14151112222";
final String targetNumber = "+14151113333"; final String targetNumber = "+14151113333";
final UUID originalPni = UUID.randomUUID();
final UUID targetPni = UUID.randomUUID();
final Device device = generateDevice(1); final Device device = generateDevice(1);
final Account account = generateAccount(originalNumber, UUID.randomUUID(), Collections.singleton(device)); final Account account = generateAccount(originalNumber, UUID.randomUUID(), originalPni, Collections.singleton(device));
accounts.create(account); accounts.create(account);
assertThat(accounts.getByPhoneNumberIdentifier(originalPni)).isPresent();
assertPhoneNumberConstraintExists(originalNumber, account.getUuid());
assertPhoneNumberIdentifierConstraintExists(originalPni, account.getUuid());
{ {
final Optional<Account> retrieved = accounts.get(originalNumber); final Optional<Account> retrieved = accounts.getByE164(originalNumber);
assertThat(retrieved).isPresent(); assertThat(retrieved).isPresent();
verifyStoredState(originalNumber, account.getUuid(), retrieved.get(), account); verifyStoredState(originalNumber, account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), retrieved.get(), account);
} }
accounts.changeNumber(account, targetNumber); accounts.changeNumber(account, targetNumber, targetPni);
assertThat(accounts.get(originalNumber)).isEmpty(); assertThat(accounts.getByE164(originalNumber)).isEmpty();
assertThat(accounts.getByAccountIdentifier(originalPni)).isEmpty();
assertPhoneNumberConstraintDoesNotExist(originalNumber);
assertPhoneNumberIdentifierConstraintDoesNotExist(originalPni);
assertPhoneNumberConstraintExists(targetNumber, account.getUuid());
assertPhoneNumberIdentifierConstraintExists(targetPni, account.getUuid());
{ {
final Optional<Account> retrieved = accounts.get(targetNumber); final Optional<Account> retrieved = accounts.getByE164(targetNumber);
assertThat(retrieved).isPresent(); assertThat(retrieved).isPresent();
verifyStoredState(targetNumber, account.getUuid(), retrieved.get(), account); verifyStoredState(targetNumber, account.getUuid(), account.getPhoneNumberIdentifier().orElseThrow(), retrieved.get(), account);
assertThat(retrieved.get().getPhoneNumberIdentifier()).isPresent();
assertThat(retrieved.get().getPhoneNumberIdentifier().get()).isEqualTo(targetPni);
assertThat(accounts.getByPhoneNumberIdentifier(targetPni)).isPresent();
} }
} }
@ -453,16 +595,267 @@ class AccountsTest {
final String originalNumber = "+14151112222"; final String originalNumber = "+14151112222";
final String targetNumber = "+14151113333"; final String targetNumber = "+14151113333";
final UUID originalPni = UUID.randomUUID();
final UUID targetPni = UUID.randomUUID();
final Device existingDevice = generateDevice(1); final Device existingDevice = generateDevice(1);
final Account existingAccount = generateAccount(targetNumber, UUID.randomUUID(), Collections.singleton(existingDevice)); final Account existingAccount = generateAccount(targetNumber, UUID.randomUUID(), targetPni, Collections.singleton(existingDevice));
final Device device = generateDevice(1); final Device device = generateDevice(1);
final Account account = generateAccount(originalNumber, UUID.randomUUID(), Collections.singleton(device)); final Account account = generateAccount(originalNumber, UUID.randomUUID(), originalPni, Collections.singleton(device));
accounts.create(account); accounts.create(account);
accounts.create(existingAccount); accounts.create(existingAccount);
assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber)); assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber, targetPni));
assertPhoneNumberConstraintExists(originalNumber, account.getUuid());
assertPhoneNumberIdentifierConstraintExists(originalPni, account.getUuid());
assertPhoneNumberConstraintExists(targetNumber, existingAccount.getUuid());
assertPhoneNumberIdentifierConstraintExists(targetPni, existingAccount.getUuid());
}
@Test
public void testChangeNumberPhoneNumberIdentifierConflict() {
final String originalNumber = "+14151112222";
final String targetNumber = "+14151113333";
final Device device = generateDevice(1);
final Account account = generateAccount(originalNumber, UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(device));
accounts.create(account);
final UUID existingAccountIdentifier = UUID.randomUUID();
final UUID existingPhoneNumberIdentifier = UUID.randomUUID();
// Artificially inject a conflicting PNI entry
dynamoDbExtension.getDynamoDbClient().putItem(PutItemRequest.builder()
.tableName(PNI_CONSTRAINT_TABLE_NAME)
.item(Map.of(
Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(existingPhoneNumberIdentifier),
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(existingAccountIdentifier)))
.conditionExpression(
"attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID,
"#pni", Accounts.ATTR_PNI_UUID))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(existingAccountIdentifier)))
.build());
assertThrows(TransactionCanceledException.class, () -> accounts.changeNumber(account, targetNumber, existingPhoneNumberIdentifier));
}
@Test
// TODO Remove or adapt after initial PNI migration
void testReregistrationFromAccountWithoutPhoneNumberIdentifier() throws JsonProcessingException {
final String number = "+18005551234";
final UUID originalUuid = UUID.randomUUID();
// Artificially inject Dynamo items for a legacy account without an assigned PNI
{
final Account account = generateAccount(number, originalUuid, null);
final TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.item(Map.of(
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID,
"#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.ATTR_ACCOUNT_DATA,
AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
Accounts.ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
Accounts.ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
final TransactWriteItem accountPut = TransactWriteItem.builder()
.put(Put.builder()
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
.tableName(ACCOUNTS_TABLE_NAME)
.item(item)
.build())
.build();
dynamoDbExtension.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
.transactItems(phoneNumberConstraintPut, accountPut)
.build());
}
final Account reregisteredAccount = generateAccount(number, UUID.randomUUID(), UUID.randomUUID());
accounts.create(reregisteredAccount);
assertPhoneNumberConstraintExists(number, originalUuid);
assertPhoneNumberIdentifierConstraintExists(reregisteredAccount.getPhoneNumberIdentifier().orElseThrow(), originalUuid);
}
@Test
// TODO Remove or adapt after initial PNI migration
void testUpdateAccountAddingPniWithoutPhoneNumberIdentifier() throws JsonProcessingException {
final String number = "+18005551234";
final UUID uuid = UUID.randomUUID();
// Artificially inject Dynamo items for a legacy account without an assigned PNI
{
final Account account = generateAccount(number, uuid, null);
final TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.item(Map.of(
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID,
"#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.ATTR_ACCOUNT_DATA,
AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
Accounts.ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
Accounts.ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
final TransactWriteItem accountPut = TransactWriteItem.builder()
.put(Put.builder()
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
.tableName(ACCOUNTS_TABLE_NAME)
.item(item)
.build())
.build();
dynamoDbExtension.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
.transactItems(phoneNumberConstraintPut, accountPut)
.build());
}
assertThat(accounts.getByAccountIdentifier(uuid)).hasValueSatisfying(account -> {
assertThat(account.getUuid()).isEqualTo(uuid);
assertThat(account.getNumber()).isEqualTo(number);
assertThat(account.getPhoneNumberIdentifier()).isEmpty();
});
final UUID phoneNumberIdentifier = UUID.randomUUID();
{
final Account accountToUpdate = accounts.getByAccountIdentifier(uuid).orElseThrow();
accountToUpdate.setNumber(number, phoneNumberIdentifier);
assertThat(accountToUpdate.getPhoneNumberIdentifier()).hasValueSatisfying(pni ->
assertThat(pni).isEqualTo(phoneNumberIdentifier));
accounts.update(accountToUpdate);
assertThat(accountToUpdate.getPhoneNumberIdentifier()).hasValueSatisfying(pni ->
assertThat(pni).isEqualTo(phoneNumberIdentifier));
}
assertThat(accounts.getByAccountIdentifier(uuid)).hasValueSatisfying(account -> {
assertThat(account.getUuid()).isEqualTo(uuid);
assertThat(account.getNumber()).isEqualTo(number);
assertThat(account.getPhoneNumberIdentifier()).hasValueSatisfying(pni ->
assertThat(pni).isEqualTo(phoneNumberIdentifier));
});
}
@Test
// TODO Remove or adapt after initial PNI migration
void testUpdateAccountWithoutPhoneNumberIdentifier() throws JsonProcessingException {
final String number = "+18005551234";
final UUID uuid = UUID.randomUUID();
// Artificially inject Dynamo items for a legacy account without an assigned PNI
{
final Account account = generateAccount(number, uuid, null);
final TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
.put(
Put.builder()
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.item(Map.of(
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.conditionExpression(
"attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
.expressionAttributeNames(
Map.of("#uuid", Accounts.KEY_ACCOUNT_UUID,
"#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(
Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build())
.build();
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
Accounts.ATTR_ACCOUNT_DATA,
AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
Accounts.ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
Accounts.ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
final TransactWriteItem accountPut = TransactWriteItem.builder()
.put(Put.builder()
.conditionExpression("attribute_not_exists(#number) OR #number = :number")
.expressionAttributeNames(Map.of("#number", Accounts.ATTR_ACCOUNT_E164))
.expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
.tableName(ACCOUNTS_TABLE_NAME)
.item(item)
.build())
.build();
dynamoDbExtension.getDynamoDbClient().transactWriteItems(TransactWriteItemsRequest.builder()
.transactItems(phoneNumberConstraintPut, accountPut)
.build());
}
assertThat(accounts.getByAccountIdentifier(uuid)).hasValueSatisfying(account -> {
assertThat(account.getUuid()).isEqualTo(uuid);
assertThat(account.getNumber()).isEqualTo(number);
assertThat(account.getPhoneNumberIdentifier()).isEmpty();
});
final String updatedName = "An updated name!";
{
final Account accountToUpdate = accounts.getByAccountIdentifier(uuid).orElseThrow();
accountToUpdate.setProfileName(updatedName);
accounts.update(accountToUpdate);
}
assertThat(accounts.getByAccountIdentifier(uuid)).hasValueSatisfying(account -> {
assertThat(account.getUuid()).isEqualTo(uuid);
assertThat(account.getNumber()).isEqualTo(number);
assertThat(account.getPhoneNumberIdentifier()).isEmpty();
assertThat(account.getProfileName()).isEqualTo(updatedName);
});
} }
private Device generateDevice(long id) { private Device generateDevice(long id) {
@ -473,20 +866,62 @@ class AccountsTest {
random.nextBoolean(), random.nextBoolean(), random.nextBoolean())); random.nextBoolean(), random.nextBoolean(), random.nextBoolean()));
} }
private Account generateAccount(String number, UUID uuid) { private Account generateAccount(String number, UUID uuid, final UUID pni) {
Device device = generateDevice(1); Device device = generateDevice(1);
return generateAccount(number, uuid, Collections.singleton(device)); return generateAccount(number, uuid, pni, Collections.singleton(device));
} }
private Account generateAccount(String number, UUID uuid, Set<Device> devices) { private Account generateAccount(String number, UUID uuid, final UUID pni, Set<Device> devices) {
byte[] unidentifiedAccessKey = new byte[16]; byte[] unidentifiedAccessKey = new byte[16];
Random random = new Random(System.currentTimeMillis()); Random random = new Random(System.currentTimeMillis());
Arrays.fill(unidentifiedAccessKey, (byte)random.nextInt(255)); Arrays.fill(unidentifiedAccessKey, (byte)random.nextInt(255));
return new Account(number, uuid, devices, unidentifiedAccessKey); return new Account(number, uuid, pni, devices, unidentifiedAccessKey);
} }
private void verifyStoredState(String number, UUID uuid, Account expecting, boolean canonicallyDiscoverable) { private void assertPhoneNumberConstraintExists(final String number, final UUID uuid) {
final GetItemResponse numberConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
GetItemRequest.builder()
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
.build());
assertThat(numberConstraintResponse.hasItem()).isTrue();
assertThat(AttributeValues.getUUID(numberConstraintResponse.item(), Accounts.KEY_ACCOUNT_UUID, null)).isEqualTo(uuid);
}
private void assertPhoneNumberConstraintDoesNotExist(final String number) {
final GetItemResponse numberConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
GetItemRequest.builder()
.tableName(NUMBER_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
.build());
assertThat(numberConstraintResponse.hasItem()).isFalse();
}
private void assertPhoneNumberIdentifierConstraintExists(final UUID phoneNumberIdentifier, final UUID uuid) {
final GetItemResponse pniConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
GetItemRequest.builder()
.tableName(PNI_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.build());
assertThat(pniConstraintResponse.hasItem()).isTrue();
assertThat(AttributeValues.getUUID(pniConstraintResponse.item(), Accounts.KEY_ACCOUNT_UUID, null)).isEqualTo(uuid);
}
private void assertPhoneNumberIdentifierConstraintDoesNotExist(final UUID phoneNumberIdentifier) {
final GetItemResponse pniConstraintResponse = dynamoDbExtension.getDynamoDbClient().getItem(
GetItemRequest.builder()
.tableName(PNI_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
.build());
assertThat(pniConstraintResponse.hasItem()).isFalse();
}
private void verifyStoredState(String number, UUID uuid, UUID pni, Account expecting, boolean canonicallyDiscoverable) {
final DynamoDbClient db = dynamoDbExtension.getDynamoDbClient(); final DynamoDbClient db = dynamoDbExtension.getDynamoDbClient();
final GetItemResponse get = db.getItem(GetItemRequest.builder() final GetItemResponse get = db.getItem(GetItemRequest.builder()
@ -506,14 +941,15 @@ class AccountsTest {
!canonicallyDiscoverable)).isEqualTo(canonicallyDiscoverable); !canonicallyDiscoverable)).isEqualTo(canonicallyDiscoverable);
Account result = Accounts.fromItem(get.item()); Account result = Accounts.fromItem(get.item());
verifyStoredState(number, uuid, result, expecting); verifyStoredState(number, uuid, pni, result, expecting);
} else { } else {
throw new AssertionError("No data"); throw new AssertionError("No data");
} }
} }
private void verifyStoredState(String number, UUID uuid, Account result, Account expecting) { private void verifyStoredState(String number, UUID uuid, UUID pni, Account result, Account expecting) {
assertThat(result.getNumber()).isEqualTo(number); assertThat(result.getNumber()).isEqualTo(number);
assertThat(result.getPhoneNumberIdentifier()).isEqualTo(Optional.ofNullable(pni));
assertThat(result.getLastSeen()).isEqualTo(expecting.getLastSeen()); assertThat(result.getLastSeen()).isEqualTo(expecting.getLastSeen());
assertThat(result.getUuid()).isEqualTo(uuid); assertThat(result.getUuid()).isEqualTo(uuid);
assertThat(result.getVersion()).isEqualTo(expecting.getVersion()); assertThat(result.getVersion()).isEqualTo(expecting.getVersion());

View File

@ -30,7 +30,7 @@ public class ContactDiscoveryWriterTest {
when(acct.getUuid()).thenReturn(uuid); when(acct.getUuid()).thenReturn(uuid);
when(acct.isCanonicallyDiscoverable()).thenReturn(canonicallyDiscoverable); when(acct.isCanonicallyDiscoverable()).thenReturn(canonicallyDiscoverable);
when(acct.shouldBeVisibleInDirectory()).thenReturn(shouldBeVisible); when(acct.shouldBeVisibleInDirectory()).thenReturn(shouldBeVisible);
when(mgr.get(uuid)).thenReturn(Optional.of(acct)); when(mgr.getByAccountIdentifier(uuid)).thenReturn(Optional.of(acct));
ContactDiscoveryWriter writer = new ContactDiscoveryWriter(mgr); ContactDiscoveryWriter writer = new ContactDiscoveryWriter(mgr);
writer.onCrawlChunk(Optional.empty(), List.of(acct)); writer.onCrawlChunk(Optional.empty(), List.of(acct));
verify(mgr, times(updateCalled ? 1 : 0)).update(acct, ContactDiscoveryWriter.NOOP_UPDATER); verify(mgr, times(updateCalled ? 1 : 0)).update(acct, ContactDiscoveryWriter.NOOP_UPDATER);

View File

@ -82,7 +82,7 @@ class MessagePersisterIntegrationTest {
when(account.getNumber()).thenReturn("+18005551234"); when(account.getNumber()).thenReturn("+18005551234");
when(account.getUuid()).thenReturn(accountUuid); when(account.getUuid()).thenReturn(accountUuid);
when(accountsManager.get(accountUuid)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(accountUuid)).thenReturn(Optional.of(account));
when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration()); when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration());
messagesCache.start(); messagesCache.start();

View File

@ -63,7 +63,7 @@ public class MessagePersisterTest extends AbstractRedisClusterTest {
final Account account = mock(Account.class); final Account account = mock(Account.class);
when(accountsManager.get(DESTINATION_ACCOUNT_UUID)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(DESTINATION_ACCOUNT_UUID)).thenReturn(Optional.of(account));
when(account.getNumber()).thenReturn(DESTINATION_ACCOUNT_NUMBER); when(account.getNumber()).thenReturn(DESTINATION_ACCOUNT_NUMBER);
when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration()); when(dynamicConfigurationManager.getConfiguration()).thenReturn(new DynamicConfiguration());
@ -98,7 +98,7 @@ public class MessagePersisterTest extends AbstractRedisClusterTest {
public void testPersistNextQueuesNoQueues() { public void testPersistNextQueuesNoQueues() {
messagePersister.persistNextQueues(Instant.now()); messagePersister.persistNextQueues(Instant.now());
verify(accountsManager, never()).get(any(UUID.class)); verify(accountsManager, never()).getByAccountIdentifier(any(UUID.class));
} }
@Test @Test
@ -147,7 +147,7 @@ public class MessagePersisterTest extends AbstractRedisClusterTest {
final Account account = mock(Account.class); final Account account = mock(Account.class);
when(accountsManager.get(accountUuid)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(accountUuid)).thenReturn(Optional.of(account));
when(account.getNumber()).thenReturn(accountNumber); when(account.getNumber()).thenReturn(accountNumber);
insertMessages(accountUuid, deviceId, messagesPerQueue, now); insertMessages(accountUuid, deviceId, messagesPerQueue, now);

View File

@ -0,0 +1,58 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
class PhoneNumberIdentifiersTest {
private static final String PNI_TABLE_NAME = "pni_test";
@RegisterExtension
static DynamoDbExtension DYNAMO_DB_EXTENSION = DynamoDbExtension.builder()
.tableName(PNI_TABLE_NAME)
.hashKey(PhoneNumberIdentifiers.KEY_E164)
.attributeDefinition(AttributeDefinition.builder()
.attributeName(PhoneNumberIdentifiers.KEY_E164)
.attributeType(ScalarAttributeType.S)
.build())
.build();
private PhoneNumberIdentifiers phoneNumberIdentifiers;
@BeforeEach
void setUp() {
phoneNumberIdentifiers = new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbClient(), PNI_TABLE_NAME);
}
@Test
void getPhoneNumberIdentifier() {
final String number = "+18005551234";
final String differentNumber = "+18005556789";
final UUID firstPni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
final UUID secondPni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
assertEquals(firstPni, secondPni);
assertNotEquals(firstPni, phoneNumberIdentifiers.getPhoneNumberIdentifier(differentNumber));
}
@Test
void generatePhoneNumberIdentifierIfNotExists() {
final String number = "+18005551234";
assertEquals(phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number),
phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number));
}
}

View File

@ -34,7 +34,7 @@ class RefreshingAccountAndDeviceSupplierTest {
when(initialDevice.getId()).thenReturn(deviceId); when(initialDevice.getId()).thenReturn(deviceId);
when(initialAccount.getDevice(deviceId)).thenReturn(Optional.of(initialDevice)); when(initialAccount.getDevice(deviceId)).thenReturn(Optional.of(initialDevice));
when(accountsManager.get(any(UUID.class))).thenAnswer(answer -> { when(accountsManager.getByAccountIdentifier(any(UUID.class))).thenAnswer(answer -> {
final Account account = mock(Account.class); final Account account = mock(Account.class);
final Device device = mock(Device.class); final Device device = mock(Device.class);

View File

@ -224,14 +224,14 @@ class AccountControllerTest {
when(pendingAccountsManager.getCodeForNumber(SENDER_HAS_STORAGE)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null, null))); when(pendingAccountsManager.getCodeForNumber(SENDER_HAS_STORAGE)).thenReturn(Optional.of(new StoredVerificationCode("666666", System.currentTimeMillis(), null, null)));
when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null, null))); when(pendingAccountsManager.getCodeForNumber(SENDER_TRANSFER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null, null)));
when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount)); when(accountsManager.getByE164(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.get(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount)); when(accountsManager.getByE164(eq(SENDER_REG_LOCK))).thenReturn(Optional.of(senderRegLockAccount));
when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount)); when(accountsManager.getByE164(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.get(eq(SENDER))).thenReturn(Optional.empty()); when(accountsManager.getByE164(eq(SENDER))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.empty()); when(accountsManager.getByE164(eq(SENDER_OLD))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_PREAUTH))).thenReturn(Optional.empty()); when(accountsManager.getByE164(eq(SENDER_PREAUTH))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_HAS_STORAGE))).thenReturn(Optional.of(senderHasStorage)); when(accountsManager.getByE164(eq(SENDER_HAS_STORAGE))).thenReturn(Optional.of(senderHasStorage));
when(accountsManager.get(eq(SENDER_TRANSFER))).thenReturn(Optional.of(senderTransfer)); when(accountsManager.getByE164(eq(SENDER_TRANSFER))).thenReturn(Optional.of(senderTransfer));
when(accountsManager.create(any(), any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> { when(accountsManager.create(any(), any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
final Account account = mock(Account.class); final Account account = mock(Account.class);
@ -1338,7 +1338,7 @@ class AccountControllerTest {
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID()); when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount)); when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
final Response response = final Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -1368,7 +1368,7 @@ class AccountControllerTest {
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID()); when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount)); when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
final Response response = final Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -1400,7 +1400,7 @@ class AccountControllerTest {
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID()); when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount)); when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
final Response response = final Response response =
resources.getJerseyTest() resources.getJerseyTest()
@ -1432,7 +1432,7 @@ class AccountControllerTest {
when(existingAccount.getUuid()).thenReturn(UUID.randomUUID()); when(existingAccount.getUuid()).thenReturn(UUID.randomUUID());
when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock); when(existingAccount.getRegistrationLock()).thenReturn(existingRegistrationLock);
when(accountsManager.get(number)).thenReturn(Optional.of(existingAccount)); when(accountsManager.getByE164(number)).thenReturn(Optional.of(existingAccount));
final Response response = final Response response =
resources.getJerseyTest() resources.getJerseyTest()

View File

@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.tests.controllers;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.eq; import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -132,8 +131,8 @@ class DeviceControllerTest {
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn( when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(
Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis(), null, null))); Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis(), null, null)));
when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.empty()); when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.empty());
when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account)); when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account));
when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount)); when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount));
AccountsHelper.setupMockUpdate(accountsManager); AccountsHelper.setupMockUpdate(accountsManager);
} }
@ -156,7 +155,7 @@ class DeviceControllerTest {
@Test @Test
void validDeviceRegisterTest() { void validDeviceRegisterTest() {
when(accountsManager.get(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT)); when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
final Device existingDevice = mock(Device.class); final Device existingDevice = mock(Device.class);
when(existingDevice.getId()).thenReturn(Device.MASTER_ID); when(existingDevice.getId()).thenReturn(Device.MASTER_ID);

View File

@ -210,7 +210,7 @@ class DonationControllerTest {
when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(receiptExpiration); when(receiptCredentialPresentation.getReceiptExpirationTime()).thenReturn(receiptExpiration);
when(redeemedReceiptsManager.put(same(receiptSerial), eq(receiptExpiration), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn( when(redeemedReceiptsManager.put(same(receiptSerial), eq(receiptExpiration), eq(receiptLevel), eq(AuthHelper.VALID_UUID))).thenReturn(
CompletableFuture.completedFuture(Boolean.TRUE)); CompletableFuture.completedFuture(Boolean.TRUE));
when(accountsManager.get(eq(AuthHelper.VALID_UUID))).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT)); when(accountsManager.getByAccountIdentifier(eq(AuthHelper.VALID_UUID))).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
RedeemReceiptRequest request = new RedeemReceiptRequest(presentation, true, true); RedeemReceiptRequest request = new RedeemReceiptRequest(presentation, true, true);
Response response = resources.getJerseyTest() Response response = resources.getJerseyTest()

View File

@ -153,11 +153,11 @@ class KeysControllerTest {
when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER); when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER);
when(existsAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes())); when(existsAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes()));
when(accounts.get(EXISTS_NUMBER)).thenReturn(Optional.of(existsAccount)); when(accounts.getByE164(EXISTS_NUMBER)).thenReturn(Optional.of(existsAccount));
when(accounts.get(EXISTS_UUID)).thenReturn(Optional.of(existsAccount)); when(accounts.getByAccountIdentifier(EXISTS_UUID)).thenReturn(Optional.of(existsAccount));
when(accounts.get(NOT_EXISTS_NUMBER)).thenReturn(Optional.empty()); when(accounts.getByE164(NOT_EXISTS_NUMBER)).thenReturn(Optional.empty());
when(accounts.get(NOT_EXISTS_UUID)).thenReturn(Optional.empty()); when(accounts.getByAccountIdentifier(NOT_EXISTS_UUID)).thenReturn(Optional.empty());
when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter); when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);

View File

@ -160,13 +160,15 @@ class MessageControllerTest {
false, false, false))); false, false, false)));
}}; }};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, singleDeviceList, "1234".getBytes()); Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, SINGLE_DEVICE_UUID, UUID.randomUUID(),
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, MULTI_DEVICE_UUID, multiDeviceList, "1234".getBytes()); singleDeviceList, "1234".getBytes());
internationalAccount = new Account(INTERNATIONAL_RECIPIENT, INTERNATIONAL_UUID, singleDeviceList, "1234".getBytes()); Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, MULTI_DEVICE_UUID, UUID.randomUUID(),
multiDeviceList, "1234".getBytes());
internationalAccount = new Account(INTERNATIONAL_RECIPIENT, INTERNATIONAL_UUID, UUID.randomUUID(), singleDeviceList, "1234".getBytes());
when(accountsManager.get(eq(SINGLE_DEVICE_UUID))).thenReturn(Optional.of(singleDeviceAccount)); when(accountsManager.getByAccountIdentifier(eq(SINGLE_DEVICE_UUID))).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.get(eq(MULTI_DEVICE_UUID))).thenReturn(Optional.of(multiDeviceAccount)); when(accountsManager.getByAccountIdentifier(eq(MULTI_DEVICE_UUID))).thenReturn(Optional.of(multiDeviceAccount));
when(accountsManager.get(INTERNATIONAL_UUID)).thenReturn(Optional.of(internationalAccount)); when(accountsManager.getByAccountIdentifier(INTERNATIONAL_UUID)).thenReturn(Optional.of(internationalAccount));
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter); when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
} }

View File

@ -169,13 +169,13 @@ class ProfileControllerTest {
when(capabilitiesAccount.isAnnouncementGroupSupported()).thenReturn(true); when(capabilitiesAccount.isAnnouncementGroupSupported()).thenReturn(true);
when(capabilitiesAccount.isChangeNumberSupported()).thenReturn(true); when(capabilitiesAccount.isChangeNumberSupported()).thenReturn(true);
when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount)); when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount));
when(accountsManager.get(AuthHelper.VALID_UUID_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(AuthHelper.VALID_UUID_TWO)).thenReturn(Optional.of("n00bkiller"));
when(usernamesManager.get("n00bkiller")).thenReturn(Optional.of(AuthHelper.VALID_UUID_TWO)); when(usernamesManager.get("n00bkiller")).thenReturn(Optional.of(AuthHelper.VALID_UUID_TWO));
when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount)); when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount));
when(accountsManager.get(AuthHelper.VALID_UUID)).thenReturn(Optional.of(capabilitiesAccount)); when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(capabilitiesAccount));
when(profilesManager.get(eq(AuthHelper.VALID_UUID), eq("someversion"))).thenReturn(Optional.empty()); when(profilesManager.get(eq(AuthHelper.VALID_UUID), eq("someversion"))).thenReturn(Optional.empty());
when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"))).thenReturn(Optional.of(new VersionedProfile( when(profilesManager.get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"))).thenReturn(Optional.of(new VersionedProfile(
@ -208,7 +208,7 @@ class ProfileControllerTest {
assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>( assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>(
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name")); badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
verify(accountsManager).get(AuthHelper.VALID_UUID_TWO); verify(accountsManager).getByAccountIdentifier(AuthHelper.VALID_UUID_TWO);
verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO)); verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));
verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID); verify(rateLimiter, times(1)).validate(AuthHelper.VALID_UUID);
} }
@ -229,7 +229,7 @@ class ProfileControllerTest {
assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>( assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>(
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name")); badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
verify(accountsManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO)); verify(accountsManager, times(1)).getByAccountIdentifier(eq(AuthHelper.VALID_UUID_TWO));
verify(usernamesManager, times(1)).get(eq("n00bkiller")); verify(usernamesManager, times(1)).get(eq("n00bkiller"));
verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID)); verify(usernameRateLimiter, times(1)).validate(eq(AuthHelper.VALID_UUID));
} }
@ -591,7 +591,7 @@ class ProfileControllerTest {
assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>( assertThat(profile.getBadges()).hasSize(1).element(0).has(new Condition<>(
badge -> "Test Badge".equals(badge.getName()), "has badge with expected name")); badge -> "Test Badge".equals(badge.getName()), "has badge with expected name"));
verify(accountsManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO)); verify(accountsManager, times(1)).getByAccountIdentifier(eq(AuthHelper.VALID_UUID_TWO));
verify(usernamesManager, times(1)).get(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(profilesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO), eq("validversion"));

View File

@ -51,7 +51,7 @@ public class APNSenderTest {
public void setup() { public void setup() {
when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice)); when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID); when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID);
when(accountsManager.get(DESTINATION_UUID)).thenReturn(Optional.of(destinationAccount)); when(accountsManager.getByAccountIdentifier(DESTINATION_UUID)).thenReturn(Optional.of(destinationAccount));
} }
@Test @Test
@ -158,7 +158,7 @@ public class APNSenderTest {
assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER); assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER);
verifyNoMoreInteractions(apnsClient); verifyNoMoreInteractions(apnsClient);
verify(accountsManager, times(1)).get(eq(DESTINATION_UUID)); verify(accountsManager, times(1)).getByAccountIdentifier(eq(DESTINATION_UUID));
verify(destinationAccount, times(1)).getDevice(1); verify(destinationAccount, times(1)).getDevice(1);
verify(destinationDevice, times(1)).getApnId(); verify(destinationDevice, times(1)).getApnId();
verify(destinationDevice, times(1)).getPushTimestamp(); verify(destinationDevice, times(1)).getPushTimestamp();
@ -261,7 +261,7 @@ public class APNSenderTest {
assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER); assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER);
verifyNoMoreInteractions(apnsClient); verifyNoMoreInteractions(apnsClient);
verify(accountsManager, times(1)).get(eq(DESTINATION_UUID)); verify(accountsManager, times(1)).getByAccountIdentifier(eq(DESTINATION_UUID));
verify(destinationAccount, times(1)).getDevice(1); verify(destinationAccount, times(1)).getDevice(1);
verify(destinationDevice, times(1)).getApnId(); verify(destinationDevice, times(1)).getApnId();
verify(destinationDevice, times(1)).getPushTimestamp(); verify(destinationDevice, times(1)).getPushTimestamp();

View File

@ -72,7 +72,7 @@ public class GCMSenderTest {
AccountsHelper.setupMockUpdate(accountsManager); AccountsHelper.setupMockUpdate(accountsManager);
when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice)); when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
when(accountsManager.get(destinationUuid)).thenReturn(Optional.of(destinationAccount)); when(accountsManager.getByAccountIdentifier(destinationUuid)).thenReturn(Optional.of(destinationAccount));
when(destinationDevice.getGcmId()).thenReturn(gcmId); when(destinationDevice.getGcmId()).thenReturn(gcmId);
when(invalidResult.isInvalidRegistrationId()).thenReturn(true); when(invalidResult.isInvalidRegistrationId()).thenReturn(true);
@ -90,7 +90,7 @@ public class GCMSenderTest {
gcmSender.sendMessage(message); gcmSender.sendMessage(message);
verify(sender, times(1)).send(any(Message.class)); verify(sender, times(1)).send(any(Message.class));
verify(accountsManager, times(1)).get(eq(destinationUuid)); verify(accountsManager, times(1)).getByAccountIdentifier(eq(destinationUuid));
verify(accountsManager, times(1)).updateDevice(eq(destinationAccount), eq(1L), any()); verify(accountsManager, times(1)).updateDevice(eq(destinationAccount), eq(1L), any());
verify(destinationDevice, times(1)).setUninstalledFeedbackTimestamp(eq(Util.todayInMillis())); verify(destinationDevice, times(1)).setUninstalledFeedbackTimestamp(eq(Util.todayInMillis()));
} }
@ -110,7 +110,7 @@ public class GCMSenderTest {
Device destinationDevice = mock(Device.class ); Device destinationDevice = mock(Device.class );
when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice)); when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
when(accountsManager.get(destinationUuid)).thenReturn(Optional.of(destinationAccount)); when(accountsManager.getByAccountIdentifier(destinationUuid)).thenReturn(Optional.of(destinationAccount));
when(destinationDevice.getGcmId()).thenReturn(gcmId); when(destinationDevice.getGcmId()).thenReturn(gcmId);
AccountsHelper.setupMockUpdate(accountsManager); AccountsHelper.setupMockUpdate(accountsManager);
@ -131,7 +131,7 @@ public class GCMSenderTest {
gcmSender.sendMessage(message); gcmSender.sendMessage(message);
verify(sender, times(1)).send(any(Message.class)); verify(sender, times(1)).send(any(Message.class));
verify(accountsManager, times(1)).get(eq(destinationUuid)); verify(accountsManager, times(1)).getByAccountIdentifier(eq(destinationUuid));
verify(accountsManager, times(1)).updateDevice(eq(destinationAccount), eq(1L), any()); verify(accountsManager, times(1)).updateDevice(eq(destinationAccount), eq(1L), any());
verify(destinationDevice, times(1)).setGcmId(eq(canonicalId)); verify(destinationDevice, times(1)).setGcmId(eq(canonicalId));
} }

View File

@ -158,26 +158,30 @@ class AccountTest {
when(disabledMasterDevice.getId()).thenReturn(1L); when(disabledMasterDevice.getId()).thenReturn(1L);
when(disabledLinkedDevice.getId()).thenReturn(2L); when(disabledLinkedDevice.getId()).thenReturn(2L);
assertTrue( new Account("+14151234567", UUID.randomUUID(), Set.of(enabledMasterDevice), new byte[0]).isEnabled()); assertTrue( new Account("+14151234567", UUID.randomUUID(), UUID.randomUUID(), Set.of(enabledMasterDevice), new byte[0]).isEnabled());
assertTrue( new Account("+14151234567", UUID.randomUUID(), Set.of(enabledMasterDevice, enabledLinkedDevice), new byte[0]).isEnabled()); assertTrue( new Account("+14151234567", UUID.randomUUID(), UUID.randomUUID(),
assertTrue( new Account("+14151234567", UUID.randomUUID(), Set.of(enabledMasterDevice, disabledLinkedDevice), new byte[0]).isEnabled()); Set.of(enabledMasterDevice, enabledLinkedDevice), new byte[0]).isEnabled());
assertFalse(new Account("+14151234567", UUID.randomUUID(), Set.of(disabledMasterDevice), new byte[0]).isEnabled()); assertTrue( new Account("+14151234567", UUID.randomUUID(), UUID.randomUUID(),
assertFalse(new Account("+14151234567", UUID.randomUUID(), Set.of(disabledMasterDevice, enabledLinkedDevice), new byte[0]).isEnabled()); Set.of(enabledMasterDevice, disabledLinkedDevice), new byte[0]).isEnabled());
assertFalse(new Account("+14151234567", UUID.randomUUID(), Set.of(disabledMasterDevice, disabledLinkedDevice), new byte[0]).isEnabled()); assertFalse(new Account("+14151234567", UUID.randomUUID(), UUID.randomUUID(), Set.of(disabledMasterDevice), new byte[0]).isEnabled());
assertFalse(new Account("+14151234567", UUID.randomUUID(), UUID.randomUUID(),
Set.of(disabledMasterDevice, enabledLinkedDevice), new byte[0]).isEnabled());
assertFalse(new Account("+14151234567", UUID.randomUUID(), UUID.randomUUID(),
Set.of(disabledMasterDevice, disabledLinkedDevice), new byte[0]).isEnabled());
} }
@Test @Test
void testCapabilities() { void testCapabilities() {
Account uuidCapable = new Account("+14152222222", UUID.randomUUID(), new HashSet<Device>() {{ Account uuidCapable = new Account("+14152222222", UUID.randomUUID(), UUID.randomUUID(), new HashSet<Device>() {{
add(gv2CapableDevice); add(gv2CapableDevice);
}}, "1234".getBytes()); }}, "1234".getBytes());
Account uuidIncapable = new Account("+14152222222", UUID.randomUUID(), new HashSet<Device>() {{ Account uuidIncapable = new Account("+14152222222", UUID.randomUUID(), UUID.randomUUID(), new HashSet<Device>() {{
add(gv2CapableDevice); add(gv2CapableDevice);
add(gv2IncapableDevice); add(gv2IncapableDevice);
}}, "1234".getBytes()); }}, "1234".getBytes());
Account uuidCapableWithExpiredIncapable = new Account("+14152222222", UUID.randomUUID(), new HashSet<Device>() {{ Account uuidCapableWithExpiredIncapable = new Account("+14152222222", UUID.randomUUID(), UUID.randomUUID(), new HashSet<Device>() {{
add(gv2CapableDevice); add(gv2CapableDevice);
add(gv2IncapableExpiredDevice); add(gv2IncapableExpiredDevice);
}}, "1234".getBytes()); }}, "1234".getBytes());
@ -213,20 +217,20 @@ class AccountTest {
{ {
final Account transferableMasterAccount = final Account transferableMasterAccount =
new Account("+14152222222", UUID.randomUUID(), Collections.singleton(transferCapableMasterDevice), "1234".getBytes()); new Account("+14152222222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(transferCapableMasterDevice), "1234".getBytes());
assertTrue(transferableMasterAccount.isTransferSupported()); assertTrue(transferableMasterAccount.isTransferSupported());
} }
{ {
final Account nonTransferableMasterAccount = final Account nonTransferableMasterAccount =
new Account("+14152222222", UUID.randomUUID(), Collections.singleton(nonTransferCapableMasterDevice), "1234".getBytes()); new Account("+14152222222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(nonTransferCapableMasterDevice), "1234".getBytes());
assertFalse(nonTransferableMasterAccount.isTransferSupported()); assertFalse(nonTransferableMasterAccount.isTransferSupported());
} }
{ {
final Account transferableLinkedAccount = new Account("+14152222222", UUID.randomUUID(), new HashSet<>() {{ final Account transferableLinkedAccount = new Account("+14152222222", UUID.randomUUID(), UUID.randomUUID(), new HashSet<>() {{
add(nonTransferCapableMasterDevice); add(nonTransferCapableMasterDevice);
add(transferCapableLinkedDevice); add(transferCapableLinkedDevice);
}}, "1234".getBytes()); }}, "1234".getBytes());
@ -237,7 +241,7 @@ class AccountTest {
@Test @Test
void testDiscoverableByPhoneNumber() { void testDiscoverableByPhoneNumber() {
final Account account = new Account("+14152222222", UUID.randomUUID(), Collections.singleton(recentMasterDevice), final Account account = new Account("+14152222222", UUID.randomUUID(), UUID.randomUUID(), Collections.singleton(recentMasterDevice),
"1234".getBytes()); "1234".getBytes());
assertTrue(account.isDiscoverableByPhoneNumber(), assertTrue(account.isDiscoverableByPhoneNumber(),
@ -252,66 +256,70 @@ class AccountTest {
@Test @Test
void isGroupsV2Supported() { void isGroupsV2Supported() {
assertTrue(new Account("+18005551234", UUID.randomUUID(), Set.of(gv2CapableDevice), assertTrue(new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(), Set.of(gv2CapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isGroupsV2Supported()); "1234".getBytes(StandardCharsets.UTF_8)).isGroupsV2Supported());
assertTrue(new Account("+18005551234", UUID.randomUUID(), Set.of(gv2CapableDevice, gv2IncapableExpiredDevice), assertTrue(new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
Set.of(gv2CapableDevice, gv2IncapableExpiredDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isGroupsV2Supported()); "1234".getBytes(StandardCharsets.UTF_8)).isGroupsV2Supported());
assertFalse(new Account("+18005551234", UUID.randomUUID(), Set.of(gv2CapableDevice, gv2IncapableDevice), assertFalse(new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
Set.of(gv2CapableDevice, gv2IncapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isGroupsV2Supported()); "1234".getBytes(StandardCharsets.UTF_8)).isGroupsV2Supported());
} }
@Test @Test
void isGv1MigrationSupported() { void isGv1MigrationSupported() {
assertTrue(new Account("+18005551234", UUID.randomUUID(), Set.of(gv1MigrationCapableDevice), assertTrue(new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(), Set.of(gv1MigrationCapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isGv1MigrationSupported()); "1234".getBytes(StandardCharsets.UTF_8)).isGv1MigrationSupported());
assertFalse( assertFalse(
new Account("+18005551234", UUID.randomUUID(), Set.of(gv1MigrationCapableDevice, gv1MigrationIncapableDevice), new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
Set.of(gv1MigrationCapableDevice, gv1MigrationIncapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isGv1MigrationSupported()); "1234".getBytes(StandardCharsets.UTF_8)).isGv1MigrationSupported());
assertTrue(new Account("+18005551234", UUID.randomUUID(), assertTrue(new Account("+18005551234", UUID.randomUUID(),
Set.of(gv1MigrationCapableDevice, gv1MigrationIncapableExpiredDevice), "1234".getBytes(StandardCharsets.UTF_8)) UUID.randomUUID(), Set.of(gv1MigrationCapableDevice, gv1MigrationIncapableExpiredDevice), "1234".getBytes(StandardCharsets.UTF_8))
.isGv1MigrationSupported()); .isGv1MigrationSupported());
} }
@Test @Test
void isSenderKeySupported() { void isSenderKeySupported() {
assertThat(new Account("+18005551234", UUID.randomUUID(), Set.of(senderKeyCapableDevice), assertThat(new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(), Set.of(senderKeyCapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isSenderKeySupported()).isTrue(); "1234".getBytes(StandardCharsets.UTF_8)).isSenderKeySupported()).isTrue();
assertThat(new Account("+18005551234", UUID.randomUUID(), Set.of(senderKeyCapableDevice, senderKeyIncapableDevice), assertThat(new Account("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
Set.of(senderKeyCapableDevice, senderKeyIncapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isSenderKeySupported()).isFalse(); "1234".getBytes(StandardCharsets.UTF_8)).isSenderKeySupported()).isFalse();
assertThat(new Account("+18005551234", UUID.randomUUID(), assertThat(new Account("+18005551234", UUID.randomUUID(),
Set.of(senderKeyCapableDevice, senderKeyIncapableExpiredDevice), UUID.randomUUID(), Set.of(senderKeyCapableDevice, senderKeyIncapableExpiredDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isSenderKeySupported()).isTrue(); "1234".getBytes(StandardCharsets.UTF_8)).isSenderKeySupported()).isTrue();
} }
@Test @Test
void isAnnouncementGroupSupported() { void isAnnouncementGroupSupported() {
assertThat(new Account("+18005551234", UUID.randomUUID(), assertThat(new Account("+18005551234", UUID.randomUUID(),
Set.of(announcementGroupCapableDevice), UUID.randomUUID(), Set.of(announcementGroupCapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isAnnouncementGroupSupported()).isTrue(); "1234".getBytes(StandardCharsets.UTF_8)).isAnnouncementGroupSupported()).isTrue();
assertThat(new Account("+18005551234", UUID.randomUUID(), assertThat(new Account("+18005551234", UUID.randomUUID(),
Set.of(announcementGroupCapableDevice, announcementGroupIncapableDevice), UUID.randomUUID(), Set.of(announcementGroupCapableDevice, announcementGroupIncapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isAnnouncementGroupSupported()).isFalse(); "1234".getBytes(StandardCharsets.UTF_8)).isAnnouncementGroupSupported()).isFalse();
assertThat(new Account("+18005551234", UUID.randomUUID(), assertThat(new Account("+18005551234", UUID.randomUUID(),
Set.of(announcementGroupCapableDevice, announcementGroupIncapableExpiredDevice), UUID.randomUUID(), Set.of(announcementGroupCapableDevice, announcementGroupIncapableExpiredDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isAnnouncementGroupSupported()).isTrue(); "1234".getBytes(StandardCharsets.UTF_8)).isAnnouncementGroupSupported()).isTrue();
} }
@Test @Test
void isChangeNumberSupported() { void isChangeNumberSupported() {
assertThat(new Account("+18005551234", UUID.randomUUID(), assertThat(new Account("+18005551234", UUID.randomUUID(),
Set.of(changeNumberCapableDevice), UUID.randomUUID(), Set.of(changeNumberCapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isChangeNumberSupported()).isTrue(); "1234".getBytes(StandardCharsets.UTF_8)).isChangeNumberSupported()).isTrue();
assertThat(new Account("+18005551234", UUID.randomUUID(), assertThat(new Account("+18005551234", UUID.randomUUID(),
Set.of(changeNumberCapableDevice, changeNumberIncapableDevice), UUID.randomUUID(), Set.of(changeNumberCapableDevice, changeNumberIncapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isChangeNumberSupported()).isFalse(); "1234".getBytes(StandardCharsets.UTF_8)).isChangeNumberSupported()).isFalse();
assertThat(new Account("+18005551234", UUID.randomUUID(), assertThat(new Account("+18005551234", UUID.randomUUID(),
Set.of(changeNumberCapableDevice, changeNumberIncapableExpiredDevice), UUID.randomUUID(), Set.of(changeNumberCapableDevice, changeNumberIncapableExpiredDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isChangeNumberSupported()).isTrue(); "1234".getBytes(StandardCharsets.UTF_8)).isChangeNumberSupported()).isTrue();
} }
@Test @Test
void stale() { void stale() {
final Account account = new Account("+14151234567", UUID.randomUUID(), Collections.emptySet(), new byte[0]); final Account account = new Account("+14151234567", UUID.randomUUID(), UUID.randomUUID(), Collections.emptySet(), new byte[0]);
assertDoesNotThrow(account::getNumber); assertDoesNotThrow(account::getNumber);
@ -327,7 +335,7 @@ class AccountTest {
final Set<Device> devices = new HashSet<>(); final Set<Device> devices = new HashSet<>();
devices.add(createDevice(Device.MASTER_ID)); devices.add(createDevice(Device.MASTER_ID));
final Account account = new Account("+14151234567", UUID.randomUUID(), devices, new byte[0]); final Account account = new Account("+14151234567", UUID.randomUUID(), UUID.randomUUID(), devices, new byte[0]);
assertThat(account.getNextDeviceId()).isEqualTo(2L); assertThat(account.getNextDeviceId()).isEqualTo(2L);
@ -348,7 +356,7 @@ class AccountTest {
@Test @Test
void addAndRemoveBadges() { void addAndRemoveBadges() {
final Account account = new Account("+14151234567", UUID.randomUUID(), Set.of(createDevice(Device.MASTER_ID)), new byte[0]); final Account account = new Account("+14151234567", UUID.randomUUID(), UUID.randomUUID(), Set.of(createDevice(Device.MASTER_ID)), new byte[0]);
final Clock clock = mock(Clock.class); final Clock clock = mock(Clock.class);
when(clock.instant()).thenReturn(Instant.ofEpochSecond(40)); when(clock.instant()).thenReturn(Instant.ofEpochSecond(40));

View File

@ -27,7 +27,9 @@ import io.lettuce.core.RedisException;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import java.time.Clock; import java.time.Clock;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
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;
@ -56,6 +58,7 @@ import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.storage.KeysDynamoDb; import org.whispersystems.textsecuregcm.storage.KeysDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
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;
@ -96,11 +99,12 @@ class AccountsManagerTest {
doAnswer((Answer<Void>) invocation -> { doAnswer((Answer<Void>) invocation -> {
final Account account = invocation.getArgument(0, Account.class); final Account account = invocation.getArgument(0, Account.class);
final String number = invocation.getArgument(1, String.class); final String number = invocation.getArgument(1, String.class);
final UUID phoneNumberIdentifier = invocation.getArgument(2, UUID.class);
account.setNumber(number); account.setNumber(number, phoneNumberIdentifier);
return null; return null;
}).when(accounts).changeNumber(any(), anyString()); }).when(accounts).changeNumber(any(), anyString(), any());
doAnswer(invocation -> { doAnswer(invocation -> {
//noinspection unchecked //noinspection unchecked
@ -114,8 +118,17 @@ class AccountsManagerTest {
final SecureBackupClient backupClient = mock(SecureBackupClient.class); final SecureBackupClient backupClient = mock(SecureBackupClient.class);
when(backupClient.deleteBackups(any())).thenReturn(CompletableFuture.completedFuture(null)); when(backupClient.deleteBackups(any())).thenReturn(CompletableFuture.completedFuture(null));
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
final Map<String, UUID> phoneNumberIdentifiersByE164 = new HashMap<>();
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(anyString())).thenAnswer((Answer<UUID>) invocation -> {
final String number = invocation.getArgument(0, String.class);
return phoneNumberIdentifiersByE164.computeIfAbsent(number, n -> UUID.randomUUID());
});
accountsManager = new AccountsManager( accountsManager = new AccountsManager(
accounts, accounts,
phoneNumberIdentifiers,
RedisClusterHelper.buildMockRedisCluster(commands), RedisClusterHelper.buildMockRedisCluster(commands),
deletedAccountsManager, deletedAccountsManager,
directoryQueue, directoryQueue,
@ -135,13 +148,14 @@ class AccountsManagerTest {
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(uuid.toString()); when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(uuid.toString());
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\"}"); when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
Optional<Account> account = accountsManager.get("+14152222222"); Optional<Account> account = accountsManager.getByE164("+14152222222");
assertTrue(account.isPresent()); assertTrue(account.isPresent());
assertEquals(account.get().getNumber(), "+14152222222"); assertEquals(account.get().getNumber(), "+14152222222");
assertEquals(account.get().getProfileName(), "test"); assertEquals(account.get().getProfileName(), "test");
assertEquals(Optional.of(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e")), account.get().getPhoneNumberIdentifier());
verify(commands, times(1)).get(eq("AccountMap::+14152222222")); verify(commands, times(1)).get(eq("AccountMap::+14152222222"));
verify(commands, times(1)).get(eq("Account3::" + uuid)); verify(commands, times(1)).get(eq("Account3::" + uuid));
@ -154,14 +168,15 @@ class AccountsManagerTest {
void testGetAccountByUuidInCache() { void testGetAccountByUuidInCache() {
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\"}"); when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
Optional<Account> account = accountsManager.get(uuid); Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
assertTrue(account.isPresent()); assertTrue(account.isPresent());
assertEquals(account.get().getNumber(), "+14152222222"); assertEquals(account.get().getNumber(), "+14152222222");
assertEquals(account.get().getUuid(), uuid); assertEquals(account.get().getUuid(), uuid);
assertEquals(account.get().getProfileName(), "test"); assertEquals(account.get().getProfileName(), "test");
assertEquals(Optional.of(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e")), account.get().getPhoneNumberIdentifier());
verify(commands, times(1)).get(eq("Account3::" + uuid)); verify(commands, times(1)).get(eq("Account3::" + uuid));
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
@ -169,110 +184,189 @@ class AccountsManagerTest {
verifyNoInteractions(accounts); verifyNoInteractions(accounts);
} }
@Test
void testGetByPniInCache() {
UUID uuid = UUID.randomUUID();
UUID pni = UUID.randomUUID();
when(commands.get(eq("AccountMap::" + pni))).thenReturn(uuid.toString());
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"name\": \"test\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
Optional<Account> account = accountsManager.getByPhoneNumberIdentifier(pni);
assertTrue(account.isPresent());
assertEquals(account.get().getNumber(), "+14152222222");
assertEquals(account.get().getProfileName(), "test");
assertEquals(Optional.of(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e")), account.get().getPhoneNumberIdentifier());
verify(commands).get(eq("AccountMap::" + pni));
verify(commands).get(eq("Account3::" + uuid));
verifyNoMoreInteractions(commands);
verifyNoInteractions(accounts);
}
@Test @Test
void testGetAccountByNumberNotInCache() { void testGetAccountByNumberNotInCache() {
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); UUID pni = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, pni, new HashSet<>(), new byte[16]);
when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(null); when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(null);
when(accounts.get(eq("+14152222222"))).thenReturn(Optional.of(account)); when(accounts.getByE164(eq("+14152222222"))).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.get("+14152222222"); Optional<Account> retrieved = accountsManager.getByE164("+14152222222");
assertTrue(retrieved.isPresent()); assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account); assertSame(retrieved.get(), account);
verify(commands, times(1)).get(eq("AccountMap::+14152222222")); verify(commands, times(1)).get(eq("AccountMap::+14152222222"));
verify(commands, times(1)).set(eq("AccountMap::+14152222222"), eq(uuid.toString())); verify(commands, times(1)).set(eq("AccountMap::+14152222222"), eq(uuid.toString()));
verify(commands, times(1)).set(eq("AccountMap::" + pni), eq(uuid.toString()));
verify(commands, times(1)).set(eq("Account3::" + uuid), anyString()); verify(commands, times(1)).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verify(accounts, times(1)).get(eq("+14152222222")); verify(accounts, times(1)).getByE164(eq("+14152222222"));
verifyNoMoreInteractions(accounts); verifyNoMoreInteractions(accounts);
} }
@Test @Test
void testGetAccountByUuidNotInCache() { void testGetAccountByUuidNotInCache() {
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); UUID pni = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, pni, new HashSet<>(), new byte[16]);
when(commands.get(eq("Account3::" + uuid))).thenReturn(null); when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accounts.get(eq(uuid))).thenReturn(Optional.of(account)); when(accounts.getByAccountIdentifier(eq(uuid))).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.get(uuid); Optional<Account> retrieved = accountsManager.getByAccountIdentifier(uuid);
assertTrue(retrieved.isPresent()); assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account); assertSame(retrieved.get(), account);
verify(commands, times(1)).get(eq("Account3::" + uuid)); verify(commands, times(1)).get(eq("Account3::" + uuid));
verify(commands, times(1)).set(eq("AccountMap::+14152222222"), eq(uuid.toString())); verify(commands, times(1)).set(eq("AccountMap::+14152222222"), eq(uuid.toString()));
verify(commands, times(1)).set(eq("AccountMap::" + pni), eq(uuid.toString()));
verify(commands, times(1)).set(eq("Account3::" + uuid), anyString()); verify(commands, times(1)).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verify(accounts, times(1)).get(eq(uuid)); verify(accounts, times(1)).getByAccountIdentifier(eq(uuid));
verifyNoMoreInteractions(accounts);
}
@Test
void testGetAccountByPniNotInCache() {
UUID uuid = UUID.randomUUID();
UUID pni = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, pni, new HashSet<>(), new byte[16]);
when(commands.get(eq("AccountMap::" + pni))).thenReturn(null);
when(accounts.getByPhoneNumberIdentifier(pni)).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.getByPhoneNumberIdentifier(pni);
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account);
verify(commands).get(eq("AccountMap::" + pni));
verify(commands).set(eq("AccountMap::" + pni), eq(uuid.toString()));
verify(commands).set(eq("AccountMap::+14152222222"), eq(uuid.toString()));
verify(commands).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands);
verify(accounts).getByPhoneNumberIdentifier(pni);
verifyNoMoreInteractions(accounts); verifyNoMoreInteractions(accounts);
} }
@Test @Test
void testGetAccountByNumberBrokenCache() { void testGetAccountByNumberBrokenCache() {
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); UUID pni = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, pni, 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(accounts.get(eq("+14152222222"))).thenReturn(Optional.of(account)); when(accounts.getByE164(eq("+14152222222"))).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.get("+14152222222"); Optional<Account> retrieved = accountsManager.getByE164("+14152222222");
assertTrue(retrieved.isPresent()); assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account); assertSame(retrieved.get(), account);
verify(commands, times(1)).get(eq("AccountMap::+14152222222")); verify(commands, times(1)).get(eq("AccountMap::+14152222222"));
verify(commands, times(1)).set(eq("AccountMap::+14152222222"), eq(uuid.toString())); verify(commands, times(1)).set(eq("AccountMap::+14152222222"), eq(uuid.toString()));
verify(commands, times(1)).set(eq("AccountMap::" + pni), eq(uuid.toString()));
verify(commands, times(1)).set(eq("Account3::" + uuid), anyString()); verify(commands, times(1)).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verify(accounts, times(1)).get(eq("+14152222222")); verify(accounts, times(1)).getByE164(eq("+14152222222"));
verifyNoMoreInteractions(accounts); verifyNoMoreInteractions(accounts);
} }
@Test @Test
void testGetAccountByUuidBrokenCache() { void testGetAccountByUuidBrokenCache() {
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, new HashSet<>(), new byte[16]); UUID pni = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, pni, 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(accounts.get(eq(uuid))).thenReturn(Optional.of(account)); when(accounts.getByAccountIdentifier(eq(uuid))).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.get(uuid); Optional<Account> retrieved = accountsManager.getByAccountIdentifier(uuid);
assertTrue(retrieved.isPresent()); assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account); assertSame(retrieved.get(), account);
verify(commands, times(1)).get(eq("Account3::" + uuid)); verify(commands, times(1)).get(eq("Account3::" + uuid));
verify(commands, times(1)).set(eq("AccountMap::+14152222222"), eq(uuid.toString())); verify(commands, times(1)).set(eq("AccountMap::+14152222222"), eq(uuid.toString()));
verify(commands, times(1)).set(eq("AccountMap::" + pni), eq(uuid.toString()));
verify(commands, times(1)).set(eq("Account3::" + uuid), anyString()); verify(commands, times(1)).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands); verifyNoMoreInteractions(commands);
verify(accounts, times(1)).get(eq(uuid)); verify(accounts, times(1)).getByAccountIdentifier(eq(uuid));
verifyNoMoreInteractions(accounts);
}
@Test
void testGetAccountByPniBrokenCache() {
UUID uuid = UUID.randomUUID();
UUID pni = UUID.randomUUID();
Account account = new Account("+14152222222", uuid, pni, new HashSet<>(), new byte[16]);
when(commands.get(eq("AccountMap::" + pni))).thenThrow(new RedisException("OH NO"));
when(accounts.getByPhoneNumberIdentifier(pni)).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.getByPhoneNumberIdentifier(pni);
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account);
verify(commands).get(eq("AccountMap::" + pni));
verify(commands).set(eq("AccountMap::" + pni), eq(uuid.toString()));
verify(commands).set(eq("AccountMap::+14152222222"), eq(uuid.toString()));
verify(commands).set(eq("Account3::" + uuid), anyString());
verifyNoMoreInteractions(commands);
verify(accounts).getByPhoneNumberIdentifier(pni);
verifyNoMoreInteractions(accounts); verifyNoMoreInteractions(accounts);
} }
@Test @Test
void testUpdate_optimisticLockingFailure() { void testUpdate_optimisticLockingFailure() {
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, UUID.randomUUID(), new HashSet<>(), new byte[16]);
when(commands.get(eq("Account3::" + uuid))).thenReturn(null); when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accounts.get(uuid)).thenReturn( when(accounts.getByAccountIdentifier(uuid)).thenReturn(
Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16]))); Optional.of(new Account("+14152222222", uuid, UUID.randomUUID(), new HashSet<>(), new byte[16])));
doThrow(ContestedOptimisticLockException.class) doThrow(ContestedOptimisticLockException.class)
.doAnswer(ACCOUNT_UPDATE_ANSWER) .doAnswer(ACCOUNT_UPDATE_ANSWER)
.when(accounts).update(any()); .when(accounts).update(any());
when(accounts.get(uuid)).thenReturn( when(accounts.getByAccountIdentifier(uuid)).thenReturn(
Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16]))); Optional.of(new Account("+14152222222", uuid, UUID.randomUUID(), new HashSet<>(), new byte[16])));
doThrow(ContestedOptimisticLockException.class) doThrow(ContestedOptimisticLockException.class)
.doAnswer(ACCOUNT_UPDATE_ANSWER) .doAnswer(ACCOUNT_UPDATE_ANSWER)
.when(accounts).update(any()); .when(accounts).update(any());
@ -282,7 +376,7 @@ class AccountsManagerTest {
assertEquals(1, account.getVersion()); assertEquals(1, account.getVersion());
assertEquals("name", account.getProfileName()); assertEquals("name", account.getProfileName());
verify(accounts, times(1)).get(uuid); verify(accounts, times(1)).getByAccountIdentifier(uuid);
verify(accounts, times(2)).update(any()); verify(accounts, times(2)).update(any());
verifyNoMoreInteractions(accounts); verifyNoMoreInteractions(accounts);
} }
@ -290,10 +384,10 @@ class AccountsManagerTest {
@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, UUID.randomUUID(), new HashSet<>(), new byte[16]);
when(commands.get(eq("Account3::" + uuid))).thenReturn(null); when(commands.get(eq("Account3::" + uuid))).thenReturn(null);
when(accounts.get(uuid)).thenReturn(Optional.empty()) when(accounts.getByAccountIdentifier(uuid)).thenReturn(Optional.empty())
.thenReturn(Optional.of(account)); .thenReturn(Optional.of(account));
when(accounts.create(any())).thenThrow(ContestedOptimisticLockException.class); when(accounts.create(any())).thenThrow(ContestedOptimisticLockException.class);
@ -307,10 +401,10 @@ class AccountsManagerTest {
@Test @Test
void testUpdateDevice() { void testUpdateDevice() {
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, UUID.randomUUID(), new HashSet<>(), new byte[16]);
when(accounts.get(uuid)).thenReturn( when(accounts.getByAccountIdentifier(uuid)).thenReturn(
Optional.of(new Account("+14152222222", uuid, new HashSet<>(), new byte[16]))); Optional.of(new Account("+14152222222", uuid, UUID.randomUUID(), new HashSet<>(), new byte[16])));
assertTrue(account.getDevices().isEmpty()); assertTrue(account.getDevices().isEmpty());
@ -422,7 +516,7 @@ class AccountsManagerTest {
@MethodSource @MethodSource
void testUpdateDirectoryQueue(final boolean visibleBeforeUpdate, final boolean visibleAfterUpdate, void testUpdateDirectoryQueue(final boolean visibleBeforeUpdate, final boolean visibleAfterUpdate,
final boolean expectRefresh) { final boolean expectRefresh) {
final Account account = new Account("+14152222222", UUID.randomUUID(), new HashSet<>(), new byte[16]); final Account account = new Account("+14152222222", UUID.randomUUID(), UUID.randomUUID(), new HashSet<>(), new byte[16]);
// this sets up the appropriate result for Account#shouldBeVisibleInDirectory // this sets up the appropriate result for Account#shouldBeVisibleInDirectory
final Device device = new Device(Device.MASTER_ID, "device", "token", "salt", null, null, null, true, 1, final Device device = new Device(Device.MASTER_ID, "device", "token", "salt", null, null, null, true, 1,
@ -449,7 +543,7 @@ class AccountsManagerTest {
@ParameterizedTest @ParameterizedTest
@MethodSource @MethodSource
void testUpdateDeviceLastSeen(final boolean expectUpdate, final long initialLastSeen, final long updatedLastSeen) { void testUpdateDeviceLastSeen(final boolean expectUpdate, final long initialLastSeen, final long updatedLastSeen) {
final Account account = new Account("+14152222222", UUID.randomUUID(), new HashSet<>(), new byte[16]); final Account account = new Account("+14152222222", UUID.randomUUID(), UUID.randomUUID(), new HashSet<>(), new byte[16]);
final Device device = new Device(Device.MASTER_ID, "device", "token", "salt", null, null, null, true, 1, final Device device = new Device(Device.MASTER_ID, "device", "token", "salt", null, null, null, true, 1,
new SignedPreKey(1, "key", "sig"), initialLastSeen, 0, new SignedPreKey(1, "key", "sig"), initialLastSeen, 0,
"OWT", 0, new DeviceCapabilities()); "OWT", 0, new DeviceCapabilities());
@ -479,7 +573,7 @@ class AccountsManagerTest {
final String targetNumber = "+14153333333"; final String targetNumber = "+14153333333";
final UUID uuid = UUID.randomUUID(); final UUID uuid = UUID.randomUUID();
Account account = new Account(originalNumber, uuid, new HashSet<>(), new byte[16]); Account account = new Account(originalNumber, uuid, UUID.randomUUID(), new HashSet<>(), new byte[16]);
account = accountsManager.changeNumber(account, targetNumber); account = accountsManager.changeNumber(account, targetNumber);
assertEquals(targetNumber, account.getNumber()); assertEquals(targetNumber, account.getNumber());
@ -491,7 +585,7 @@ class AccountsManagerTest {
void testChangePhoneNumberSameNumber() throws InterruptedException { void testChangePhoneNumberSameNumber() throws InterruptedException {
final String number = "+14152222222"; final String number = "+14152222222";
Account account = new Account(number, UUID.randomUUID(), new HashSet<>(), new byte[16]); Account account = new Account(number, UUID.randomUUID(), UUID.randomUUID(), new HashSet<>(), new byte[16]);
account = accountsManager.changeNumber(account, number); account = accountsManager.changeNumber(account, number);
assertEquals(number, account.getNumber()); assertEquals(number, account.getNumber());
@ -509,10 +603,10 @@ class AccountsManagerTest {
final UUID existingAccountUuid = UUID.randomUUID(); final UUID existingAccountUuid = UUID.randomUUID();
final UUID uuid = UUID.randomUUID(); final UUID uuid = UUID.randomUUID();
final Account existingAccount = new Account(targetNumber, existingAccountUuid, new HashSet<>(), new byte[16]); final Account existingAccount = new Account(targetNumber, existingAccountUuid, UUID.randomUUID(), new HashSet<>(), new byte[16]);
when(accounts.get(targetNumber)).thenReturn(Optional.of(existingAccount)); when(accounts.getByE164(targetNumber)).thenReturn(Optional.of(existingAccount));
Account account = new Account(originalNumber, uuid, new HashSet<>(), new byte[16]); Account account = new Account(originalNumber, uuid, UUID.randomUUID(), new HashSet<>(), new byte[16]);
account = accountsManager.changeNumber(account, targetNumber); account = accountsManager.changeNumber(account, targetNumber);
assertEquals(targetNumber, account.getNumber()); assertEquals(targetNumber, account.getNumber());
@ -527,8 +621,8 @@ class AccountsManagerTest {
final String targetNumber = "+14153333333"; final String targetNumber = "+14153333333";
final UUID uuid = UUID.randomUUID(); final UUID uuid = UUID.randomUUID();
final Account account = new Account(originalNumber, uuid, new HashSet<>(), new byte[16]); final Account account = new Account(originalNumber, uuid, UUID.randomUUID(), new HashSet<>(), new byte[16]);
assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setNumber(targetNumber))); assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setNumber(targetNumber, UUID.randomUUID())));
} }
} }

View File

@ -62,7 +62,7 @@ public class AccountsHelper {
} }
public static void setupMockGet(final AccountsManager mockAccountsManager, final Set<Account> mockAccounts) { public static void setupMockGet(final AccountsManager mockAccountsManager, final Set<Account> mockAccounts) {
when(mockAccountsManager.get(any(UUID.class))).thenAnswer(answer -> { when(mockAccountsManager.getByAccountIdentifier(any(UUID.class))).thenAnswer(answer -> {
final UUID uuid = answer.getArgument(0, UUID.class); final UUID uuid = answer.getArgument(0, UUID.class);
@ -176,7 +176,7 @@ public class AccountsHelper {
} else { } else {
final ObjectMapper mapper = SystemMapper.getMapper(); final ObjectMapper mapper = SystemMapper.getMapper();
updatedAccount = mapper.readValue(mapper.writeValueAsBytes(account), Account.class); updatedAccount = mapper.readValue(mapper.writeValueAsBytes(account), Account.class);
updatedAccount.setNumber(account.getNumber()); updatedAccount.setNumber(account.getNumber(), account.getPhoneNumberIdentifier().orElse(null));
account.markStale(); account.markStale();
} }

View File

@ -137,17 +137,17 @@ public class AuthHelper {
reset(ACCOUNTS_MANAGER); reset(ACCOUNTS_MANAGER);
when(ACCOUNTS_MANAGER.get(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT)); when(ACCOUNTS_MANAGER.getByE164(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT));
when(ACCOUNTS_MANAGER.get(VALID_UUID)).thenReturn(Optional.of(VALID_ACCOUNT)); when(ACCOUNTS_MANAGER.getByAccountIdentifier(VALID_UUID)).thenReturn(Optional.of(VALID_ACCOUNT));
when(ACCOUNTS_MANAGER.get(VALID_NUMBER_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO)); when(ACCOUNTS_MANAGER.getByE164(VALID_NUMBER_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO));
when(ACCOUNTS_MANAGER.get(VALID_UUID_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO)); when(ACCOUNTS_MANAGER.getByAccountIdentifier(VALID_UUID_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO));
when(ACCOUNTS_MANAGER.get(DISABLED_NUMBER)).thenReturn(Optional.of(DISABLED_ACCOUNT)); when(ACCOUNTS_MANAGER.getByE164(DISABLED_NUMBER)).thenReturn(Optional.of(DISABLED_ACCOUNT));
when(ACCOUNTS_MANAGER.get(DISABLED_UUID)).thenReturn(Optional.of(DISABLED_ACCOUNT)); when(ACCOUNTS_MANAGER.getByAccountIdentifier(DISABLED_UUID)).thenReturn(Optional.of(DISABLED_ACCOUNT));
when(ACCOUNTS_MANAGER.get(UNDISCOVERABLE_NUMBER)).thenReturn(Optional.of(UNDISCOVERABLE_ACCOUNT)); when(ACCOUNTS_MANAGER.getByE164(UNDISCOVERABLE_NUMBER)).thenReturn(Optional.of(UNDISCOVERABLE_ACCOUNT));
when(ACCOUNTS_MANAGER.get(UNDISCOVERABLE_UUID)).thenReturn(Optional.of(UNDISCOVERABLE_ACCOUNT)); when(ACCOUNTS_MANAGER.getByAccountIdentifier(UNDISCOVERABLE_UUID)).thenReturn(Optional.of(UNDISCOVERABLE_ACCOUNT));
AccountsHelper.setupMockUpdateForAuthHelper(ACCOUNTS_MANAGER); AccountsHelper.setupMockUpdateForAuthHelper(ACCOUNTS_MANAGER);
@ -220,8 +220,8 @@ public class AuthHelper {
when(account.getUuid()).thenReturn(uuid); when(account.getUuid()).thenReturn(uuid);
when(account.getRelay()).thenReturn(Optional.empty()); when(account.getRelay()).thenReturn(Optional.empty());
when(account.isEnabled()).thenReturn(true); when(account.isEnabled()).thenReturn(true);
when(accountsManager.get(number)).thenReturn(Optional.of(account)); when(accountsManager.getByE164(number)).thenReturn(Optional.of(account));
when(accountsManager.get(uuid)).thenReturn(Optional.of(account)); when(accountsManager.getByAccountIdentifier(uuid)).thenReturn(Optional.of(account));
} }
} }

View File

@ -172,8 +172,8 @@ public class WebSocketConnectionTest {
Account sender1 = mock(Account.class); Account sender1 = mock(Account.class);
when(sender1.getDevices()).thenReturn(sender1devices); when(sender1.getDevices()).thenReturn(sender1devices);
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1)); when(accountsManager.getByE164("sender1")).thenReturn(Optional.of(sender1));
when(accountsManager.get("sender2")).thenReturn(Optional.empty()); when(accountsManager.getByE164("sender2")).thenReturn(Optional.empty());
String userAgent = "user-agent"; String userAgent = "user-agent";
@ -327,8 +327,8 @@ public class WebSocketConnectionTest {
Account sender1 = mock(Account.class); Account sender1 = mock(Account.class);
when(sender1.getDevices()).thenReturn(sender1devices); when(sender1.getDevices()).thenReturn(sender1devices);
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1)); when(accountsManager.getByE164("sender1")).thenReturn(Optional.of(sender1));
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>empty()); when(accountsManager.getByE164("sender2")).thenReturn(Optional.<Account>empty());
String userAgent = "user-agent"; String userAgent = "user-agent";
@ -700,8 +700,8 @@ public class WebSocketConnectionTest {
Account sender1 = mock(Account.class); Account sender1 = mock(Account.class);
when(sender1.getDevices()).thenReturn(sender1devices); when(sender1.getDevices()).thenReturn(sender1devices);
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1)); when(accountsManager.getByE164("sender1")).thenReturn(Optional.of(sender1));
when(accountsManager.get("sender2")).thenReturn(Optional.empty()); when(accountsManager.getByE164("sender2")).thenReturn(Optional.empty());
String userAgent = "Signal-Desktop/1.2.3"; String userAgent = "Signal-Desktop/1.2.3";
@ -776,8 +776,8 @@ public class WebSocketConnectionTest {
Account sender1 = mock(Account.class); Account sender1 = mock(Account.class);
when(sender1.getDevices()).thenReturn(sender1devices); when(sender1.getDevices()).thenReturn(sender1devices);
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1)); when(accountsManager.getByE164("sender1")).thenReturn(Optional.of(sender1));
when(accountsManager.get("sender2")).thenReturn(Optional.empty()); when(accountsManager.getByE164("sender2")).thenReturn(Optional.empty());
String userAgent = "Signal-Android/4.68.3"; String userAgent = "Signal-Android/4.68.3";