Accept source ACI at `/v1/messages/report`
This commit is contained in:
parent
fa3a9570d6
commit
77fd01bd9f
|
@ -637,8 +637,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||
ReceiptCredentialPresentation::new, stripeExecutor, config.getDonationConfiguration(), config.getStripe()),
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, apnFallbackManager,
|
||||
reportMessageManager, multiRecipientMessageExecutor),
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager,
|
||||
messagesManager, apnFallbackManager, reportMessageManager, multiRecipientMessageExecutor),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager, profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations),
|
||||
new ProvisioningController(rateLimiters, provisioningManager),
|
||||
|
|
|
@ -88,6 +88,7 @@ import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
|||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
|
@ -105,14 +106,15 @@ public class MessageController {
|
|||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final MessageSender messageSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
private final AccountsManager accountsManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final ReportMessageManager reportMessageManager;
|
||||
private final ExecutorService multiRecipientMessageExecutor;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final MessageSender messageSender;
|
||||
private final ReceiptSender receiptSender;
|
||||
private final AccountsManager accountsManager;
|
||||
private final DeletedAccountsManager deletedAccountsManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final ReportMessageManager reportMessageManager;
|
||||
private final ExecutorService multiRecipientMessageExecutor;
|
||||
|
||||
@VisibleForTesting
|
||||
static final Semver FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE = new Semver("5.22.0");
|
||||
|
@ -146,6 +148,7 @@ public class MessageController {
|
|||
MessageSender messageSender,
|
||||
ReceiptSender receiptSender,
|
||||
AccountsManager accountsManager,
|
||||
DeletedAccountsManager deletedAccountsManager,
|
||||
MessagesManager messagesManager,
|
||||
ApnFallbackManager apnFallbackManager,
|
||||
ReportMessageManager reportMessageManager,
|
||||
|
@ -154,6 +157,7 @@ public class MessageController {
|
|||
this.messageSender = messageSender;
|
||||
this.receiptSender = receiptSender;
|
||||
this.accountsManager = accountsManager;
|
||||
this.deletedAccountsManager = deletedAccountsManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
this.reportMessageManager = reportMessageManager;
|
||||
|
@ -556,11 +560,39 @@ public class MessageController {
|
|||
|
||||
@Timed
|
||||
@POST
|
||||
@Path("/report/{sourceNumber}/{messageGuid}")
|
||||
public Response reportMessage(@Auth AuthenticatedAccount auth, @PathParam("sourceNumber") String sourceNumber,
|
||||
@Path("/report/{source}/{messageGuid}")
|
||||
public Response reportMessage(@Auth AuthenticatedAccount auth, @PathParam("source") String source,
|
||||
@PathParam("messageGuid") UUID messageGuid) {
|
||||
|
||||
reportMessageManager.report(sourceNumber, messageGuid, auth.getAccount().getUuid());
|
||||
final Optional<String> sourceNumber;
|
||||
final Optional<UUID> sourceAci;
|
||||
final Optional<UUID> sourcePni;
|
||||
if (source.startsWith("+")) {
|
||||
sourceNumber = Optional.of(source);
|
||||
final Optional<Account> maybeAccount = accountsManager.getByE164(source);
|
||||
if (maybeAccount.isPresent()) {
|
||||
sourceAci = maybeAccount.map(Account::getUuid);
|
||||
sourcePni = maybeAccount.map(Account::getPhoneNumberIdentifier);
|
||||
} else {
|
||||
sourceAci = deletedAccountsManager.findDeletedAccountAci(source);
|
||||
sourcePni = Optional.ofNullable(accountsManager.getPhoneNumberIdentifier(source));
|
||||
}
|
||||
} else {
|
||||
sourceAci = Optional.of(UUID.fromString(source));
|
||||
|
||||
final Optional<Account> sourceAccount = accountsManager.getByAccountIdentifier(sourceAci.get());
|
||||
|
||||
if (sourceAccount.isEmpty()) {
|
||||
logger.warn("Could not find source: {}", sourceAci.get());
|
||||
sourceNumber = deletedAccountsManager.findDeletedAccountE164(sourceAci.get());
|
||||
sourcePni = sourceNumber.map(accountsManager::getPhoneNumberIdentifier);
|
||||
} else {
|
||||
sourceNumber = sourceAccount.map(Account::getNumber);
|
||||
sourcePni = sourceAccount.map(Account::getPhoneNumberIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
reportMessageManager.report(sourceNumber, sourceAci, sourcePni, messageGuid, auth.getAccount().getUuid());
|
||||
|
||||
return Response.status(Status.ACCEPTED)
|
||||
.build();
|
||||
|
|
|
@ -513,6 +513,14 @@ public class AccountsManager {
|
|||
}
|
||||
}
|
||||
|
||||
public Optional<String> getNumberForPhoneNumberIdentifier(UUID pni) {
|
||||
return phoneNumberIdentifiers.getPhoneNumber(pni);
|
||||
}
|
||||
|
||||
public UUID getPhoneNumberIdentifier(String e164) {
|
||||
return phoneNumberIdentifiers.getPhoneNumberIdentifier(e164);
|
||||
}
|
||||
|
||||
public AccountCrawlChunk getAllFromDynamo(int length) {
|
||||
return accounts.getAllFromStart(length);
|
||||
}
|
||||
|
|
|
@ -21,14 +21,16 @@ import java.util.stream.Collectors;
|
|||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes;
|
||||
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||
|
||||
|
@ -40,6 +42,8 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
|
|||
static final String ATTR_EXPIRES = "E";
|
||||
static final String ATTR_NEEDS_CDS_RECONCILIATION = "R";
|
||||
|
||||
static final String UUID_TO_E164_INDEX_NAME = "u_to_p";
|
||||
|
||||
static final Duration TIME_TO_LIVE = Duration.ofDays(30);
|
||||
|
||||
// Note that this limit is imposed by DynamoDB itself; going above 100 will result in errors
|
||||
|
@ -76,6 +80,28 @@ public class DeletedAccounts extends AbstractDynamoDbStore {
|
|||
return Optional.ofNullable(AttributeValues.getUUID(response.item(), ATTR_ACCOUNT_UUID, null));
|
||||
}
|
||||
|
||||
Optional<String> findE164(final UUID uuid) {
|
||||
final QueryResponse response = db().query(QueryRequest.builder()
|
||||
.tableName(tableName)
|
||||
.indexName(UUID_TO_E164_INDEX_NAME)
|
||||
.keyConditionExpression("#uuid = :uuid")
|
||||
.projectionExpression("#e164")
|
||||
.expressionAttributeNames(Map.of("#uuid", ATTR_ACCOUNT_UUID,
|
||||
"#e164", KEY_ACCOUNT_E164))
|
||||
.expressionAttributeValues(Map.of(":uuid", AttributeValues.fromUUID(uuid))).build());
|
||||
|
||||
if (response.count() == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (response.count() > 1) {
|
||||
throw new RuntimeException(
|
||||
"Impossible result: more than one phone number returned for UUID: " + uuid);
|
||||
}
|
||||
|
||||
return Optional.ofNullable(response.items().get(0).get(KEY_ACCOUNT_E164).s());
|
||||
}
|
||||
|
||||
void remove(final String e164) {
|
||||
db().deleteItem(DeleteItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
|
|
|
@ -173,8 +173,7 @@ public class DeletedAccountsManager {
|
|||
}
|
||||
|
||||
return lockAcquired;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}).toList();
|
||||
|
||||
assert lockItems.size() == reconciliationCandidates.size();
|
||||
|
||||
|
@ -192,7 +191,16 @@ public class DeletedAccountsManager {
|
|||
try {
|
||||
deletedAccounts.markReconciled(consumer.reconcile(accountsToReconcile));
|
||||
} finally {
|
||||
lockItems.forEach(lockItem -> lockClient.releaseLock(ReleaseLockOptions.builder(lockItem).withBestEffort(true).build()));
|
||||
lockItems.forEach(
|
||||
lockItem -> lockClient.releaseLock(ReleaseLockOptions.builder(lockItem).withBestEffort(true).build()));
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<UUID> findDeletedAccountAci(final String e164) {
|
||||
return deletedAccounts.findUuid(e164);
|
||||
}
|
||||
|
||||
public Optional<String> findDeletedAccountE164(final UUID uuid) {
|
||||
return deletedAccounts.findE164(uuid);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import java.util.List;
|
|||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
|
@ -23,6 +25,8 @@ import org.whispersystems.textsecuregcm.util.Constants;
|
|||
|
||||
public class MessagesManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessagesManager.class);
|
||||
|
||||
private static final int RESULT_SET_CHUNK_SIZE = 100;
|
||||
|
||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
||||
|
@ -53,7 +57,11 @@ public class MessagesManager {
|
|||
messagesCache.insert(messageGuid, destinationUuid, destinationDevice, message);
|
||||
|
||||
if (message.hasSource() && !destinationUuid.toString().equals(message.getSourceUuid())) {
|
||||
reportMessageManager.store(message.getSource(), messageGuid);
|
||||
if (message.hasSourceUuid()) {
|
||||
reportMessageManager.store(message.getSource(), message.getSourceUuid(), messageGuid);
|
||||
} else {
|
||||
logger.warn("Message missing source UUID");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,20 +5,23 @@
|
|||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
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.QueryRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
|
||||
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
|
||||
|
@ -31,7 +34,10 @@ public class PhoneNumberIdentifiers {
|
|||
|
||||
@VisibleForTesting
|
||||
static final String KEY_E164 = "P";
|
||||
private static final String ATTR_PHONE_NUMBER_IDENTIFIER = "PNI";
|
||||
@VisibleForTesting
|
||||
static final String INDEX_NAME = "pni_to_p";
|
||||
@VisibleForTesting
|
||||
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"));
|
||||
|
@ -69,6 +75,34 @@ public class PhoneNumberIdentifiers {
|
|||
return phoneNumberIdentifier;
|
||||
}
|
||||
|
||||
public Optional<String> getPhoneNumber(final UUID phoneNumberIdentifier) {
|
||||
final QueryResponse response = dynamoDbClient.query(QueryRequest.builder()
|
||||
.tableName(tableName)
|
||||
.indexName(INDEX_NAME)
|
||||
.keyConditionExpression("#pni = :pni")
|
||||
.projectionExpression("#phone_number")
|
||||
.expressionAttributeNames(Map.of(
|
||||
"#phone_number", KEY_E164,
|
||||
"#pni", ATTR_PHONE_NUMBER_IDENTIFIER
|
||||
))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":pni", AttributeValues.fromUUID(phoneNumberIdentifier)
|
||||
))
|
||||
.build());
|
||||
|
||||
if (response.count() == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (response.count() > 1) {
|
||||
throw new RuntimeException(
|
||||
"Impossible result: more than one phone number returned for PNI: " + phoneNumberIdentifier);
|
||||
}
|
||||
|
||||
return Optional.ofNullable(response.items().get(0).get(KEY_E164).s());
|
||||
}
|
||||
|
||||
|
||||
@VisibleForTesting
|
||||
UUID generatePhoneNumberIdentifierIfNotExists(final String phoneNumber) {
|
||||
final UpdateItemResponse response = SET_PNI_TIMER.record(() -> dynamoDbClient.updateItem(UpdateItemRequest.builder()
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import io.lettuce.core.RedisException;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
@ -8,6 +11,7 @@ import java.time.Duration;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -16,6 +20,8 @@ import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
|||
|
||||
public class ReportMessageManager {
|
||||
|
||||
private static final String MIGRATION_COUNTER_NAME = name(ReportMessageManager.class, "migration");
|
||||
|
||||
private final ReportMessageDynamoDb reportMessageDynamoDb;
|
||||
private final FaultTolerantRedisCluster rateLimitCluster;
|
||||
|
||||
|
@ -39,51 +45,91 @@ public class ReportMessageManager {
|
|||
this.reportedMessageListeners.add(listener);
|
||||
}
|
||||
|
||||
public void store(String sourceNumber, UUID messageGuid) {
|
||||
// TODO sourceNumber can be removed after 2022-04-01
|
||||
public void store(String sourceNumber, String sourceAci, UUID messageGuid) {
|
||||
|
||||
try {
|
||||
Objects.requireNonNull(sourceNumber);
|
||||
Objects.requireNonNull(sourceAci);
|
||||
|
||||
reportMessageDynamoDb.store(hash(messageGuid, sourceNumber));
|
||||
reportMessageDynamoDb.store(hash(messageGuid, sourceAci));
|
||||
} catch (final Exception e) {
|
||||
logger.warn("Failed to store hash", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void report(String sourceNumber, UUID messageGuid, UUID reporterUuid) {
|
||||
public void report(Optional<String> sourceNumber, Optional<UUID> sourceAci, Optional<UUID> sourcePni,
|
||||
UUID messageGuid, UUID reporterUuid) {
|
||||
|
||||
final boolean found = reportMessageDynamoDb.remove(hash(messageGuid, sourceNumber));
|
||||
// TODO sourceNumber can be removed after 2022-04-15
|
||||
final boolean foundByNumber = sourceNumber.map(number -> reportMessageDynamoDb.remove(hash(messageGuid, number)))
|
||||
.orElse(false);
|
||||
|
||||
if (found) {
|
||||
final boolean foundByAci = sourceAci.map(uuid -> reportMessageDynamoDb.remove(hash(messageGuid, uuid.toString()))).
|
||||
orElse(false);
|
||||
|
||||
if (foundByNumber || foundByAci) {
|
||||
rateLimitCluster.useCluster(connection -> {
|
||||
final String reportedSenderKey = getReportedSenderKey(sourceNumber);
|
||||
sourceNumber.ifPresent(number -> {
|
||||
final String reportedSenderKey = getReportedSenderKey(number);
|
||||
connection.sync().pfadd(reportedSenderKey, reporterUuid.toString());
|
||||
connection.sync().expire(reportedSenderKey, counterTtl.toSeconds());
|
||||
});
|
||||
|
||||
connection.sync().pfadd(reportedSenderKey, reporterUuid.toString());
|
||||
connection.sync().expire(reportedSenderKey, counterTtl.toSeconds());
|
||||
sourcePni.ifPresent(pni -> {
|
||||
final String reportedSenderKey = getReportedSenderPniKey(pni);
|
||||
connection.sync().pfadd(reportedSenderKey, reporterUuid.toString());
|
||||
connection.sync().expire(reportedSenderKey, counterTtl.toSeconds());
|
||||
});
|
||||
|
||||
sourceAci.ifPresent(aci -> {
|
||||
final String reportedSenderKey = getReportedSenderAciKey(aci);
|
||||
connection.sync().pfadd(reportedSenderKey, reporterUuid.toString());
|
||||
connection.sync().expire(reportedSenderKey, counterTtl.toSeconds());
|
||||
});
|
||||
});
|
||||
|
||||
reportedMessageListeners.forEach(listener -> {
|
||||
try {
|
||||
listener.handleMessageReported(sourceNumber, messageGuid, reporterUuid);
|
||||
} catch (final Exception e) {
|
||||
logger.error("Failed to notify listener of reported message", e);
|
||||
}
|
||||
});
|
||||
sourceNumber.ifPresent(number ->
|
||||
reportedMessageListeners.forEach(listener -> {
|
||||
try {
|
||||
// TODO should listener take the source Aci?
|
||||
listener.handleMessageReported(number, messageGuid, reporterUuid);
|
||||
} catch (final Exception e) {
|
||||
logger.error("Failed to notify listener of reported message", e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Metrics.counter(
|
||||
MIGRATION_COUNTER_NAME,
|
||||
"foundByNumber", String.valueOf(foundByNumber),
|
||||
"foundByAci", String.valueOf(foundByAci),
|
||||
"sourceAciPresent", String.valueOf(sourceAci.isPresent()),
|
||||
"sourcePniPresent", String.valueOf(sourcePni.isPresent()),
|
||||
"sourceNumberPresent", String.valueOf(sourceNumber.isPresent())
|
||||
).increment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of times messages from the given number have been reported by recipients as abusive. Note that
|
||||
* Returns the number of times messages from the given account have been reported by recipients as abusive. Note that
|
||||
* this method makes a call to an external service, and callers should take care to memoize calls where possible and
|
||||
* avoid unnecessary calls.
|
||||
*
|
||||
* @param number the number to check for recent reports
|
||||
*
|
||||
* @param account the account to check for recent reports
|
||||
* @return the number of times the given number has been reported recently
|
||||
*/
|
||||
public int getRecentReportCount(final String number) {
|
||||
public int getRecentReportCount(final Account account) {
|
||||
try {
|
||||
return rateLimitCluster.withCluster(connection -> connection.sync().pfcount(getReportedSenderKey(number)).intValue());
|
||||
return rateLimitCluster.withCluster(
|
||||
connection ->
|
||||
Math.max(
|
||||
Math.max(
|
||||
// TODO number can be removed after 2022-04-15
|
||||
connection.sync().pfcount(getReportedSenderKey(account.getNumber())).intValue(),
|
||||
connection.sync().pfcount(getReportedSenderPniKey(account.getPhoneNumberIdentifier()))
|
||||
.intValue()),
|
||||
connection.sync().pfcount(getReportedSenderAciKey(account.getUuid())).intValue()));
|
||||
} catch (final RedisException e) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -106,4 +152,12 @@ public class ReportMessageManager {
|
|||
private static String getReportedSenderKey(final String senderNumber) {
|
||||
return "reported_number::" + senderNumber;
|
||||
}
|
||||
|
||||
private static String getReportedSenderAciKey(final UUID aci) {
|
||||
return "reported_account::" + aci.toString();
|
||||
}
|
||||
|
||||
private static String getReportedSenderPniKey(final UUID pni) {
|
||||
return "reported_pni::" + pni.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ import org.whispersystems.textsecuregcm.push.MessageSender;
|
|||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.DeletedAccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||
|
@ -94,24 +95,25 @@ class MessageControllerTest {
|
|||
private static final UUID SINGLE_DEVICE_UUID = UUID.randomUUID();
|
||||
private static final UUID SINGLE_DEVICE_PNI = UUID.randomUUID();
|
||||
|
||||
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
||||
private static final UUID MULTI_DEVICE_UUID = UUID.randomUUID();
|
||||
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
|
||||
private static final UUID MULTI_DEVICE_UUID = UUID.randomUUID();
|
||||
|
||||
private static final String INTERNATIONAL_RECIPIENT = "+61123456789";
|
||||
private static final UUID INTERNATIONAL_UUID = UUID.randomUUID();
|
||||
private static final UUID INTERNATIONAL_UUID = UUID.randomUUID();
|
||||
|
||||
private Account internationalAccount;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final RedisAdvancedClusterCommands<String, String> redisCommands = mock(RedisAdvancedClusterCommands.class);
|
||||
|
||||
private static final MessageSender messageSender = mock(MessageSender.class);
|
||||
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
|
||||
private static final AccountsManager accountsManager = mock(AccountsManager.class);
|
||||
private static final MessagesManager messagesManager = mock(MessagesManager.class);
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
|
||||
private static final ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
|
||||
private static final MessageSender messageSender = mock(MessageSender.class);
|
||||
private static final ReceiptSender receiptSender = mock(ReceiptSender.class);
|
||||
private static final AccountsManager accountsManager = mock(AccountsManager.class);
|
||||
private static final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
|
||||
private static final MessagesManager messagesManager = mock(MessagesManager.class);
|
||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
|
||||
private static final ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
|
||||
private static final ReportMessageManager reportMessageManager = mock(ReportMessageManager.class);
|
||||
private static final ExecutorService multiRecipientMessageExecutor = mock(ExecutorService.class);
|
||||
|
||||
|
@ -122,8 +124,9 @@ class MessageControllerTest {
|
|||
.addProvider(RateLimitExceededExceptionMapper.class)
|
||||
.addProvider(MultiDeviceMessageListProvider.class)
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new MessageController(rateLimiters, messageSender, receiptSender, accountsManager,
|
||||
messagesManager, apnFallbackManager, reportMessageManager, multiRecipientMessageExecutor))
|
||||
.addResource(
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager,
|
||||
messagesManager, apnFallbackManager, reportMessageManager, multiRecipientMessageExecutor))
|
||||
.build();
|
||||
|
||||
@BeforeEach
|
||||
|
@ -486,13 +489,13 @@ class MessageControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void testGetMessages() throws Exception {
|
||||
void testGetMessages() {
|
||||
|
||||
final long timestampOne = 313377;
|
||||
final long timestampTwo = 313388;
|
||||
|
||||
final UUID messageGuidOne = UUID.randomUUID();
|
||||
final UUID sourceUuid = UUID.randomUUID();
|
||||
final UUID sourceUuid = UUID.randomUUID();
|
||||
|
||||
List<OutgoingMessageEntity> messages = new LinkedList<>() {{
|
||||
add(new OutgoingMessageEntity(messageGuidOne, Envelope.Type.CIPHERTEXT_VALUE, timestampOne, "+14152222222", sourceUuid, 2, AuthHelper.VALID_UUID, "hi there".getBytes(), 0));
|
||||
|
@ -595,12 +598,23 @@ class MessageControllerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void testReportMessage() {
|
||||
void testReportMessageByE164() {
|
||||
|
||||
final String senderNumber = "+12125550001";
|
||||
final UUID messageGuid = UUID.randomUUID();
|
||||
final UUID senderAci = UUID.randomUUID();
|
||||
final UUID senderPni = UUID.randomUUID();
|
||||
UUID messageGuid = UUID.randomUUID();
|
||||
|
||||
final Response response =
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getUuid()).thenReturn(senderAci);
|
||||
when(account.getNumber()).thenReturn(senderNumber);
|
||||
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
||||
|
||||
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.of(account));
|
||||
when(deletedAccountsManager.findDeletedAccountAci(senderNumber)).thenReturn(Optional.of(senderAci));
|
||||
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/report/%s/%s", senderNumber, messageGuid))
|
||||
.request()
|
||||
|
@ -609,7 +623,73 @@ class MessageControllerTest {
|
|||
|
||||
assertThat(response.getStatus(), is(equalTo(202)));
|
||||
|
||||
verify(reportMessageManager).report(senderNumber, messageGuid, AuthHelper.VALID_UUID);
|
||||
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
|
||||
messageGuid, AuthHelper.VALID_UUID);
|
||||
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
|
||||
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
|
||||
|
||||
when(accountsManager.getByE164(senderNumber)).thenReturn(Optional.empty());
|
||||
messageGuid = UUID.randomUUID();
|
||||
|
||||
response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/report/%s/%s", senderNumber, messageGuid))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(null);
|
||||
|
||||
assertThat(response.getStatus(), is(equalTo(202)));
|
||||
|
||||
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
|
||||
messageGuid, AuthHelper.VALID_UUID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReportMessageByAci() {
|
||||
|
||||
final String senderNumber = "+12125550001";
|
||||
final UUID senderAci = UUID.randomUUID();
|
||||
final UUID senderPni = UUID.randomUUID();
|
||||
UUID messageGuid = UUID.randomUUID();
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getUuid()).thenReturn(senderAci);
|
||||
when(account.getNumber()).thenReturn(senderNumber);
|
||||
when(account.getPhoneNumberIdentifier()).thenReturn(senderPni);
|
||||
|
||||
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.of(account));
|
||||
when(deletedAccountsManager.findDeletedAccountE164(senderAci)).thenReturn(Optional.of(senderNumber));
|
||||
when(accountsManager.getPhoneNumberIdentifier(senderNumber)).thenReturn(senderPni);
|
||||
|
||||
Response response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/report/%s/%s", senderAci, messageGuid))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(null);
|
||||
|
||||
assertThat(response.getStatus(), is(equalTo(202)));
|
||||
|
||||
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
|
||||
messageGuid, AuthHelper.VALID_UUID);
|
||||
verify(deletedAccountsManager, never()).findDeletedAccountE164(any(UUID.class));
|
||||
verify(accountsManager, never()).getPhoneNumberIdentifier(anyString());
|
||||
|
||||
when(accountsManager.getByAccountIdentifier(senderAci)).thenReturn(Optional.empty());
|
||||
|
||||
messageGuid = UUID.randomUUID();
|
||||
|
||||
response =
|
||||
resources.getJerseyTest()
|
||||
.target(String.format("/v1/messages/report/%s/%s", senderAci, messageGuid))
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.post(null);
|
||||
|
||||
assertThat(response.getStatus(), is(equalTo(202)));
|
||||
|
||||
verify(reportMessageManager).report(Optional.of(senderNumber), Optional.of(senderAci), Optional.of(senderPni),
|
||||
messageGuid, AuthHelper.VALID_UUID);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
|
|
@ -5,6 +5,16 @@
|
|||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.lang.Thread.State;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
@ -17,16 +27,6 @@ import software.amazon.awssdk.services.dynamodb.model.Projection;
|
|||
import software.amazon.awssdk.services.dynamodb.model.ProjectionType;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
|
||||
import java.lang.Thread.State;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class DeletedAccountsManagerTest {
|
||||
|
||||
|
@ -43,11 +43,26 @@ class DeletedAccountsManagerTest {
|
|||
.attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION)
|
||||
.attributeType(ScalarAttributeType.N)
|
||||
.build())
|
||||
.attributeDefinition(AttributeDefinition.builder()
|
||||
.attributeName(DeletedAccounts.ATTR_ACCOUNT_UUID)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
|
||||
.indexName(NEEDS_RECONCILIATION_INDEX_NAME)
|
||||
.keySchema(KeySchemaElement.builder().attributeName(DeletedAccounts.KEY_ACCOUNT_E164).keyType(KeyType.HASH).build(),
|
||||
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION).keyType(KeyType.RANGE).build())
|
||||
.projection(Projection.builder().projectionType(ProjectionType.INCLUDE).nonKeyAttributes(DeletedAccounts.ATTR_ACCOUNT_UUID).build())
|
||||
.keySchema(
|
||||
KeySchemaElement.builder().attributeName(DeletedAccounts.KEY_ACCOUNT_E164).keyType(KeyType.HASH).build(),
|
||||
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION)
|
||||
.keyType(KeyType.RANGE).build())
|
||||
.projection(Projection.builder().projectionType(ProjectionType.INCLUDE)
|
||||
.nonKeyAttributes(DeletedAccounts.ATTR_ACCOUNT_UUID).build())
|
||||
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
|
||||
.build())
|
||||
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
|
||||
.indexName(DeletedAccounts.UUID_TO_E164_INDEX_NAME)
|
||||
.keySchema(
|
||||
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_ACCOUNT_UUID).keyType(KeyType.HASH).build()
|
||||
)
|
||||
.projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build())
|
||||
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
|
||||
.build())
|
||||
.build();
|
||||
|
@ -167,4 +182,5 @@ class DeletedAccountsManagerTest {
|
|||
return List.of(deletedAccounts.get(0).second());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,11 +41,26 @@ class DeletedAccountsTest {
|
|||
.attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION)
|
||||
.attributeType(ScalarAttributeType.N)
|
||||
.build())
|
||||
.attributeDefinition(AttributeDefinition.builder()
|
||||
.attributeName(DeletedAccounts.ATTR_ACCOUNT_UUID)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
|
||||
.indexName(NEEDS_RECONCILIATION_INDEX_NAME)
|
||||
.keySchema(KeySchemaElement.builder().attributeName(DeletedAccounts.KEY_ACCOUNT_E164).keyType(KeyType.HASH).build(),
|
||||
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION).keyType(KeyType.RANGE).build())
|
||||
.projection(Projection.builder().projectionType(ProjectionType.INCLUDE).nonKeyAttributes(DeletedAccounts.ATTR_ACCOUNT_UUID).build())
|
||||
.keySchema(
|
||||
KeySchemaElement.builder().attributeName(DeletedAccounts.KEY_ACCOUNT_E164).keyType(KeyType.HASH).build(),
|
||||
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_NEEDS_CDS_RECONCILIATION)
|
||||
.keyType(KeyType.RANGE).build())
|
||||
.projection(Projection.builder().projectionType(ProjectionType.INCLUDE)
|
||||
.nonKeyAttributes(DeletedAccounts.ATTR_ACCOUNT_UUID).build())
|
||||
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
|
||||
.build())
|
||||
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
|
||||
.indexName(DeletedAccounts.UUID_TO_E164_INDEX_NAME)
|
||||
.keySchema(
|
||||
KeySchemaElement.builder().attributeName(DeletedAccounts.ATTR_ACCOUNT_UUID).keyType(KeyType.HASH).build()
|
||||
)
|
||||
.projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build())
|
||||
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
|
||||
.build())
|
||||
.build();
|
||||
|
@ -156,4 +171,30 @@ class DeletedAccountsTest {
|
|||
|
||||
assertEquals(expectedAccountsNeedingReconciliation, accountsNeedingReconciliation);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testFindE164() {
|
||||
assertEquals(Optional.empty(), deletedAccounts.findE164(UUID.randomUUID()));
|
||||
|
||||
final UUID uuid = UUID.randomUUID();
|
||||
final String e164 = "+18005551234";
|
||||
|
||||
deletedAccounts.put(uuid, e164, true);
|
||||
|
||||
assertEquals(Optional.of(e164), deletedAccounts.findE164(uuid));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindUUID() {
|
||||
final String e164 = "+18005551234";
|
||||
|
||||
assertEquals(Optional.empty(), deletedAccounts.findUuid(e164));
|
||||
|
||||
final UUID uuid = UUID.randomUUID();
|
||||
|
||||
deletedAccounts.put(uuid, e164, true);
|
||||
|
||||
assertEquals(Optional.of(uuid), deletedAccounts.findUuid(e164));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,16 +24,17 @@ class MessagesManagerTest {
|
|||
@Test
|
||||
void insert() {
|
||||
final String sourceNumber = "+12025551212";
|
||||
final UUID sourceAci = UUID.randomUUID();
|
||||
final Envelope message = Envelope.newBuilder()
|
||||
.setSource(sourceNumber)
|
||||
.setSourceUuid(UUID.randomUUID().toString())
|
||||
.setSourceUuid(sourceAci.toString())
|
||||
.build();
|
||||
|
||||
final UUID destinationUuid = UUID.randomUUID();
|
||||
|
||||
messagesManager.insert(destinationUuid, 1L, message);
|
||||
|
||||
verify(reportMessageManager).store(eq(sourceNumber), any(UUID.class));
|
||||
verify(reportMessageManager).store(eq(sourceNumber), eq(sourceAci.toString()), any(UUID.class));
|
||||
|
||||
final Envelope syncMessage = Envelope.newBuilder(message)
|
||||
.setSourceUuid(destinationUuid.toString())
|
||||
|
|
|
@ -6,13 +6,21 @@
|
|||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import java.util.Optional;
|
||||
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.GlobalSecondaryIndex;
|
||||
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
|
||||
import software.amazon.awssdk.services.dynamodb.model.KeyType;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Projection;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ProjectionType;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput;
|
||||
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
|
||||
|
||||
class PhoneNumberIdentifiersTest {
|
||||
|
@ -27,6 +35,20 @@ class PhoneNumberIdentifiersTest {
|
|||
.attributeName(PhoneNumberIdentifiers.KEY_E164)
|
||||
.attributeType(ScalarAttributeType.S)
|
||||
.build())
|
||||
.attributeDefinition(AttributeDefinition.builder()
|
||||
.attributeName(PhoneNumberIdentifiers.ATTR_PHONE_NUMBER_IDENTIFIER)
|
||||
.attributeType(ScalarAttributeType.B)
|
||||
.build())
|
||||
.globalSecondaryIndex(GlobalSecondaryIndex.builder()
|
||||
.indexName(PhoneNumberIdentifiers.INDEX_NAME)
|
||||
.projection(Projection.builder()
|
||||
.projectionType(ProjectionType.KEYS_ONLY)
|
||||
.build())
|
||||
.keySchema(KeySchemaElement.builder().keyType(KeyType.HASH)
|
||||
.attributeName(PhoneNumberIdentifiers.ATTR_PHONE_NUMBER_IDENTIFIER)
|
||||
.build())
|
||||
.provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build())
|
||||
.build())
|
||||
.build();
|
||||
|
||||
private PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||
|
@ -55,4 +77,14 @@ class PhoneNumberIdentifiersTest {
|
|||
assertEquals(phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number),
|
||||
phoneNumberIdentifiers.generatePhoneNumberIdentifierIfNotExists(number));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPhoneNumber() {
|
||||
final String number = "+18005551234";
|
||||
|
||||
assertFalse(phoneNumberIdentifiers.getPhoneNumber(UUID.randomUUID()).isPresent());
|
||||
|
||||
final UUID pni = phoneNumberIdentifiers.getPhoneNumberIdentifier(number);
|
||||
assertEquals(Optional.of(number), phoneNumberIdentifiers.getPhoneNumber(pni));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -23,8 +22,16 @@ import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
|
|||
class ReportMessageManagerTest {
|
||||
|
||||
private ReportMessageDynamoDb reportMessageDynamoDb;
|
||||
|
||||
private ReportMessageManager reportMessageManager;
|
||||
|
||||
private String sourceNumber;
|
||||
private UUID sourceAci;
|
||||
private UUID sourcePni;
|
||||
private Account sourceAccount;
|
||||
private UUID messageGuid;
|
||||
private UUID reporterUuid;
|
||||
|
||||
@RegisterExtension
|
||||
static RedisClusterExtension RATE_LIMIT_CLUSTER_EXTENSION = RedisClusterExtension.builder().build();
|
||||
|
||||
|
@ -34,77 +41,95 @@ class ReportMessageManagerTest {
|
|||
|
||||
reportMessageManager = new ReportMessageManager(reportMessageDynamoDb,
|
||||
RATE_LIMIT_CLUSTER_EXTENSION.getRedisCluster(), Duration.ofDays(1));
|
||||
|
||||
sourceNumber = "+15105551111";
|
||||
sourceAci = UUID.randomUUID();
|
||||
sourcePni = UUID.randomUUID();
|
||||
messageGuid = UUID.randomUUID();
|
||||
reporterUuid = UUID.randomUUID();
|
||||
|
||||
sourceAccount = mock(Account.class);
|
||||
when(sourceAccount.getUuid()).thenReturn(sourceAci);
|
||||
when(sourceAccount.getNumber()).thenReturn(sourceNumber);
|
||||
when(sourceAccount.getPhoneNumberIdentifier()).thenReturn(sourcePni);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStore() {
|
||||
|
||||
final UUID messageGuid = UUID.randomUUID();
|
||||
final String number = "+15105551111";
|
||||
|
||||
assertDoesNotThrow(() -> reportMessageManager.store(null, messageGuid));
|
||||
assertDoesNotThrow(() -> reportMessageManager.store(null, null, messageGuid));
|
||||
|
||||
verifyNoInteractions(reportMessageDynamoDb);
|
||||
|
||||
reportMessageManager.store(number, messageGuid);
|
||||
reportMessageManager.store(sourceNumber, sourceAci.toString(), messageGuid);
|
||||
|
||||
verify(reportMessageDynamoDb).store(any());
|
||||
verify(reportMessageDynamoDb, times(2)).store(any());
|
||||
|
||||
doThrow(RuntimeException.class)
|
||||
.when(reportMessageDynamoDb).store(any());
|
||||
|
||||
assertDoesNotThrow(() -> reportMessageManager.store(number, messageGuid));
|
||||
assertDoesNotThrow(() -> reportMessageManager.store(sourceNumber, sourceAci.toString(), messageGuid));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReport() {
|
||||
final String sourceNumber = "+15105551111";
|
||||
final UUID messageGuid = UUID.randomUUID();
|
||||
final UUID reporterUuid = UUID.randomUUID();
|
||||
|
||||
final ReportedMessageListener listener = mock(ReportedMessageListener.class);
|
||||
reportMessageManager.addListener(listener);
|
||||
|
||||
when(reportMessageDynamoDb.remove(any())).thenReturn(false);
|
||||
reportMessageManager.report(sourceNumber, messageGuid, reporterUuid);
|
||||
reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni), messageGuid,
|
||||
reporterUuid);
|
||||
|
||||
assertEquals(0, reportMessageManager.getRecentReportCount(sourceNumber));
|
||||
assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount));
|
||||
|
||||
when(reportMessageDynamoDb.remove(any())).thenReturn(true);
|
||||
reportMessageManager.report(sourceNumber, messageGuid, reporterUuid);
|
||||
reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni), messageGuid,
|
||||
reporterUuid);
|
||||
|
||||
assertEquals(1, reportMessageManager.getRecentReportCount(sourceNumber));
|
||||
assertEquals(1, reportMessageManager.getRecentReportCount(sourceAccount));
|
||||
verify(listener).handleMessageReported(sourceNumber, messageGuid, reporterUuid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReportMultipleReporters() {
|
||||
final String sourceNumber = "+15105551111";
|
||||
final UUID messageGuid = UUID.randomUUID();
|
||||
|
||||
when(reportMessageDynamoDb.remove(any())).thenReturn(true);
|
||||
assertEquals(0, reportMessageManager.getRecentReportCount(sourceNumber));
|
||||
assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount));
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
reportMessageManager.report(sourceNumber, messageGuid, UUID.randomUUID());
|
||||
reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni),
|
||||
messageGuid, UUID.randomUUID());
|
||||
}
|
||||
|
||||
assertTrue(reportMessageManager.getRecentReportCount(sourceNumber) > 10);
|
||||
assertTrue(reportMessageManager.getRecentReportCount(sourceAccount) > 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReportSingleReporter() {
|
||||
final String sourceNumber = "+15105551111";
|
||||
final UUID messageGuid = UUID.randomUUID();
|
||||
final UUID reporterUuid = UUID.randomUUID();
|
||||
|
||||
when(reportMessageDynamoDb.remove(any())).thenReturn(true);
|
||||
assertEquals(0, reportMessageManager.getRecentReportCount(sourceNumber));
|
||||
assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount));
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
reportMessageManager.report(sourceNumber, messageGuid, reporterUuid);
|
||||
reportMessageManager.report(Optional.of(sourceNumber), Optional.of(sourceAci), Optional.of(sourcePni),
|
||||
messageGuid,
|
||||
reporterUuid);
|
||||
}
|
||||
|
||||
assertEquals(1, reportMessageManager.getRecentReportCount(sourceNumber));
|
||||
assertEquals(1, reportMessageManager.getRecentReportCount(sourceAccount));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReportMultipleReportersByPni() {
|
||||
when(reportMessageDynamoDb.remove(any())).thenReturn(true);
|
||||
assertEquals(0, reportMessageManager.getRecentReportCount(sourceAccount));
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
reportMessageManager.report(Optional.of(sourceNumber), Optional.empty(), Optional.of(sourcePni),
|
||||
messageGuid, UUID.randomUUID());
|
||||
}
|
||||
|
||||
reportMessageManager.report(Optional.empty(), Optional.of(sourceAci), Optional.empty(),
|
||||
messageGuid, UUID.randomUUID());
|
||||
|
||||
final int recentReportCount = reportMessageManager.getRecentReportCount(sourceAccount);
|
||||
assertTrue(recentReportCount > 10);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue