Compare commits

...

7 Commits

Author SHA1 Message Date
Jon Chambers
08f6ec639c Update to the latest version of the spam filter 2025-12-02 15:59:54 -05:00
Jonathan Klabunde Tomer
6c3cfc88b5 retire /v1/config
It's been supplanted by /v2/config for all clients beyond the 90-day window.
We still have [some traffic](https://signal.grafana.net/goto/bf5tjk346v1moa?orgId=1)
but it's all from expired/third-party clients (note the lack of a recognized
version number in the client-version tag).
2025-12-02 12:52:39 -08:00
Jon Chambers
389d44fd80 Remove legacy delete-via-REST plumbing 2025-12-02 15:45:18 -05:00
Jon Chambers
7604306818 Retire REST-based message deletion 2025-12-02 15:45:18 -05:00
Jon Chambers
92e133b21f Shut down command dependencies in LIFO order 2025-12-02 15:45:01 -05:00
Jon Chambers
4af50986e0 Minor corrections to docs for POST /v1/registration 2025-12-02 15:44:43 -05:00
Jon Chambers
c72458b47a Perform basic input validation on call quality survey responses 2025-12-01 09:56:09 -05:00
19 changed files with 139 additions and 615 deletions

View File

@@ -4,8 +4,8 @@
*/
package org.whispersystems.textsecuregcm;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import static java.util.Objects.requireNonNull;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.google.common.collect.Lists;
import com.webauthn4j.appattest.DeviceCheckManager;
@@ -125,7 +125,6 @@ import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
import org.whispersystems.textsecuregcm.controllers.RegistrationController;
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
import org.whispersystems.textsecuregcm.controllers.RemoteConfigControllerV1;
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
import org.whispersystems.textsecuregcm.controllers.StickerController;
@@ -1085,7 +1084,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ReceiptCredentialPresentation::new),
new KeysController(rateLimiters, keysManager, accountsManager, zkSecretParams, Clock.systemUTC()),
new KeyTransparencyController(keyTransparencyServiceClient),
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender,
accountsManager, messagesManager, phoneNumberIdentifiers, pushNotificationManager, pushNotificationScheduler,
reportMessageManager, messageDeliveryScheduler, clientReleaseManager,
zkSecretParams, spamChecker, messageMetrics, messageDeliveryLoopMonitor,
@@ -1097,7 +1096,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new ProvisioningController(rateLimiters, provisioningManager),
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
rateLimiters),
new RemoteConfigControllerV1(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
new SecureStorageController(storageCredentialsGenerator),
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),

View File

@@ -66,11 +66,15 @@ public class CallQualitySurveyController {
try {
submitCallQualitySurveyRequest = SubmitCallQualitySurveyRequest.parseFrom(surveyResponse);
} catch (final InvalidProtocolBufferException e) {
throw new WebApplicationException(422);
throw new WebApplicationException("Invalid protobuf entity", 422);
}
final String remoteAddress = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
callQualitySurveyManager.submitCallQualitySurvey(submitCallQualitySurveyRequest, remoteAddress, userAgentString);
try {
callQualitySurveyManager.submitCallQualitySurvey(submitCallQualitySurveyRequest, remoteAddress, userAgentString);
} catch (final IllegalArgumentException e) {
throw new WebApplicationException(e.getMessage(), 422);
}
}
}

View File

@@ -21,7 +21,6 @@ import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
@@ -74,7 +73,6 @@ import org.whispersystems.textsecuregcm.entities.AccountStaleDevices;
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope.Type;
import org.whispersystems.textsecuregcm.entities.MismatchedDevicesResponse;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
@@ -97,7 +95,6 @@ import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
import org.whispersystems.textsecuregcm.push.MessageUtil;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.spam.MessageType;
import org.whispersystems.textsecuregcm.spam.SpamCheckResult;
import org.whispersystems.textsecuregcm.spam.SpamChecker;
@@ -123,7 +120,6 @@ public class MessageController {
private final RateLimiters rateLimiters;
private final CardinalityEstimator messageByteLimitEstimator;
private final MessageSender messageSender;
private final ReceiptSender receiptSender;
private final AccountsManager accountsManager;
private final MessagesManager messagesManager;
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
@@ -177,7 +173,6 @@ public class MessageController {
RateLimiters rateLimiters,
CardinalityEstimator messageByteLimitEstimator,
MessageSender messageSender,
ReceiptSender receiptSender,
AccountsManager accountsManager,
MessagesManager messagesManager,
PhoneNumberIdentifiers phoneNumberIdentifiers,
@@ -194,7 +189,6 @@ public class MessageController {
this.rateLimiters = rateLimiters;
this.messageByteLimitEstimator = messageByteLimitEstimator;
this.messageSender = messageSender;
this.receiptSender = receiptSender;
this.accountsManager = accountsManager;
this.messagesManager = messagesManager;
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
@@ -843,37 +837,6 @@ public class MessageController {
return size;
}
@DELETE
@Path("/uuid/{uuid}")
public CompletableFuture<Response> removePendingMessage(@Auth AuthenticatedDevice auth, @PathParam("uuid") UUID uuid) {
final Account account = accountsManager.getByAccountIdentifier(auth.accountIdentifier())
.orElseThrow(() -> new WebApplicationException(Status.UNAUTHORIZED));
final Device device = account.getDevice(auth.deviceId())
.orElseThrow(() -> new WebApplicationException(Status.UNAUTHORIZED));
return messagesManager.delete(auth.accountIdentifier(), device, uuid, null)
.thenAccept(maybeRemovedMessage -> maybeRemovedMessage.ifPresent(removedMessage -> {
if (removedMessage.sourceServiceId().isPresent()
&& removedMessage.envelopeType() != Type.SERVER_DELIVERY_RECEIPT) {
if (removedMessage.sourceServiceId().get() instanceof AciServiceIdentifier aciServiceIdentifier) {
try {
receiptSender.sendReceipt(removedMessage.destinationServiceId(), auth.deviceId(),
aciServiceIdentifier, removedMessage.clientTimestamp());
} catch (Exception e) {
logger.warn("Failed to send delivery receipt", e);
}
} else {
// If source service ID is present and the envelope type is not a server delivery receipt, then
// the source service ID *should always* be an ACI -- PNIs are receive-only, so they can only be the
// "source" via server delivery receipts
logger.warn("Source service ID unexpectedly a PNI service ID");
}
}
}))
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("/report/{source}/{messageGuid}")

View File

@@ -92,7 +92,8 @@ public class RegistrationController {
2. gets 409 from device available for transfer \n
3. success \n
""")
@ApiResponse(responseCode = "200", description = "The phone number associated with the authenticated account was changed successfully", useReturnTypeSchema = true)
@ApiResponse(responseCode = "200", description = "Account creation succeeded", useReturnTypeSchema = true)
@ApiResponse(responseCode = "401", description = "The session identified in the request is not verified")
@ApiResponse(responseCode = "403", description = "Verification failed for the provided Registration Recovery Password")
@ApiResponse(responseCode = "409", description = "The caller has not explicitly elected to skip transferring data from another device, but a device transfer is technically possible")
@ApiResponse(responseCode = "422", description = "The request did not pass validation")

View File

@@ -1,107 +0,0 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import com.google.common.annotations.VisibleForTesting;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
import org.whispersystems.textsecuregcm.util.Conversions;
import org.whispersystems.textsecuregcm.util.Util;
@Path("/v1/config")
@Tag(name = "Remote Config")
public class RemoteConfigControllerV1 {
private final RemoteConfigsManager remoteConfigsManager;
private final Map<String, String> globalConfig;
private final Clock clock;
private static final String GLOBAL_CONFIG_PREFIX = "global.";
public RemoteConfigControllerV1(RemoteConfigsManager remoteConfigsManager,
Map<String, String> globalConfig,
final Clock clock) {
this.remoteConfigsManager = remoteConfigsManager;
this.globalConfig = globalConfig;
this.clock = clock;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Deprecated
@Operation(
summary = "Fetch remote configuration (deprecated)",
description = """
Remote configuration is a list of namespaced keys that clients may use for consistent configuration or behavior.
Configuration values change over time, and the list should be refreshed periodically, typically at client
launch and every few hours thereafter.
This endpoint is deprecated; use GET /v2/config instead
"""
)
@ApiResponse(responseCode = "200", description = "Remote configuration values for the authenticated user", useReturnTypeSchema = true)
public UserRemoteConfigList getAll(@Auth AuthenticatedDevice auth) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
final Stream<UserRemoteConfig> globalConfigStream = globalConfig.entrySet().stream()
.map(entry -> new UserRemoteConfig(GLOBAL_CONFIG_PREFIX + entry.getKey(), true, entry.getValue()));
return new UserRemoteConfigList(Stream.concat(remoteConfigsManager.getAll().stream().map(config -> {
final byte[] hashKey = config.getHashKey() != null ? config.getHashKey().getBytes(StandardCharsets.UTF_8)
: config.getName().getBytes(StandardCharsets.UTF_8);
boolean inBucket = isInBucket(digest, auth.accountIdentifier(), hashKey, config.getPercentage(),
config.getUuids());
return new UserRemoteConfig(config.getName(), inBucket,
inBucket ? config.getValue() : config.getDefaultValue());
}), globalConfigStream).collect(Collectors.toList()), clock.instant());
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
@VisibleForTesting
public static boolean isInBucket(MessageDigest digest, UUID uid, byte[] hashKey, int configPercentage,
Set<UUID> uuidsInBucket) {
if (uuidsInBucket.contains(uid)) {
return true;
}
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(uid.getMostSignificantBits());
bb.putLong(uid.getLeastSignificantBits());
digest.update(bb.array());
byte[] hash = digest.digest(hashKey);
int bucket = (int) (Util.ensureNonNegativeLong(Conversions.byteArrayToLong(hash)) % 100);
return bucket < configPercentage;
}
}

View File

@@ -5,6 +5,8 @@
package org.whispersystems.textsecuregcm.grpc;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.signal.chat.calling.quality.SimpleCallQualityGrpc;
import org.signal.chat.calling.quality.SubmitCallQualitySurveyRequest;
import org.signal.chat.calling.quality.SubmitCallQualitySurveyResponse;
@@ -32,9 +34,13 @@ public class CallQualitySurveyGrpcService extends SimpleCallQualityGrpc.CallQual
rateLimiters.getSubmitCallQualitySurveyLimiter().validate(remoteAddress);
callQualitySurveyManager.submitCallQualitySurvey(request,
remoteAddress,
RequestAttributesUtil.getUserAgent().orElse(null));
try {
callQualitySurveyManager.submitCallQualitySurvey(request,
remoteAddress,
RequestAttributesUtil.getUserAgent().orElse(null));
} catch (final IllegalArgumentException e) {
throw Status.INVALID_ARGUMENT.withDescription(e.getMessage()).asRuntimeException();
}
return SubmitCallQualitySurveyResponse.getDefaultInstance();
}

View File

@@ -6,6 +6,7 @@
package org.whispersystems.textsecuregcm.metrics;
import com.google.cloud.pubsub.v1.PublisherInterface;
import com.google.common.annotations.VisibleForTesting;
import com.google.pubsub.v1.PubsubMessage;
import io.micrometer.core.instrument.Metrics;
import java.time.Clock;
@@ -147,4 +148,23 @@ public class CallQualitySurveyManager {
.increment();
});
}
@VisibleForTesting
static void validateRequest(final SubmitCallQualitySurveyRequest request) {
if (request.getStartTimestamp() == 0) {
throw new IllegalArgumentException("Start timestamp not specified");
}
if (request.getEndTimestamp() == 0) {
throw new IllegalArgumentException("End timestamp not specified");
}
if (StringUtils.isBlank(request.getCallType())) {
throw new IllegalArgumentException("Call type not specified");
}
if (StringUtils.isBlank(request.getCallEndReason())) {
throw new IllegalArgumentException("Call end reason not specified");
}
}
}

View File

@@ -5,8 +5,8 @@
package org.whispersystems.textsecuregcm.storage;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import static io.micrometer.core.instrument.Metrics.timer;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
@@ -30,9 +30,6 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
@@ -51,9 +48,6 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
@VisibleForTesting
static final String KEY_SORT = "S";
@VisibleForTesting
static final String LOCAL_INDEX_MESSAGE_UUID_NAME = "Message_UUID_Index";
@VisibleForTesting
static final String LOCAL_INDEX_MESSAGE_UUID_KEY_SORT = "U";
@@ -69,7 +63,6 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
private final String tableName;
private final Duration timeToLive;
private final ExecutorService messageDeletionExecutor;
private final Scheduler messageDeletionScheduler;
private final ExperimentEnrollmentManager experimentEnrollmentManager;
private static final Logger logger = LoggerFactory.getLogger(MessagesDynamoDb.class);
@@ -84,7 +77,6 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
this.timeToLive = timeToLive;
this.messageDeletionExecutor = messageDeletionExecutor;
this.messageDeletionScheduler = Schedulers.fromExecutor(messageDeletionExecutor);
this.experimentEnrollmentManager = experimentEnrollmentManager;
}
@@ -164,48 +156,6 @@ public class MessagesDynamoDb extends AbstractDynamoDbStore {
.filter(Predicate.not(Objects::isNull));
}
public CompletableFuture<Optional<MessageProtos.Envelope>> deleteMessageByDestinationAndGuid(
final UUID destinationAccountUuid, final Device destinationDevice, final UUID messageUuid) {
final AttributeValue partitionKey = convertPartitionKey(destinationAccountUuid, destinationDevice);
final QueryRequest queryRequest = QueryRequest.builder()
.tableName(tableName)
.indexName(LOCAL_INDEX_MESSAGE_UUID_NAME)
.projectionExpression(KEY_SORT)
.consistentRead(true)
.keyConditionExpression("#part = :part AND #uuid = :uuid")
.expressionAttributeNames(Map.of(
"#part", KEY_PARTITION,
"#uuid", LOCAL_INDEX_MESSAGE_UUID_KEY_SORT))
.expressionAttributeValues(Map.of(
":part", partitionKey,
":uuid", convertLocalIndexMessageUuidSortKey(messageUuid)))
.build();
// because we are filtering on message UUID, this query should return at most one item,
// but its simpler to handle the full stream and return the “last” item
return Flux.from(dbAsyncClient.queryPaginator(queryRequest).items())
.flatMap(item -> Mono.fromCompletionStage(dbAsyncClient.deleteItem(DeleteItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_PARTITION, partitionKey, KEY_SORT,
AttributeValues.fromByteArray(item.get(KEY_SORT).b().asByteArray())))
.returnValues(ReturnValue.ALL_OLD)
.build())))
.mapNotNull(deleteItemResponse -> {
try {
if (deleteItemResponse.attributes() != null && deleteItemResponse.attributes().containsKey(KEY_PARTITION)) {
return convertItemToEnvelope(deleteItemResponse.attributes(), experimentEnrollmentManager);
}
} catch (final InvalidProtocolBufferException e) {
logger.error("Failed to parse envelope", e);
}
return null;
})
.map(Optional::ofNullable)
.subscribeOn(messageDeletionScheduler)
.last(Optional.empty()) // if the flux is empty, last() will throw without a default
.toFuture();
}
public CompletableFuture<Optional<MessageProtos.Envelope>> deleteMessage(final UUID destinationAccountUuid,
final Device destinationDevice, final UUID messageUuid, final long serverTimestamp) {
DeleteItemRequest.Builder deleteItemRequest = DeleteItemRequest.builder()

View File

@@ -28,7 +28,6 @@ import org.reactivestreams.Publisher;
import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
@@ -253,26 +252,17 @@ public class MessagesManager {
return messagesCache.clear(destinationUuid, deviceId);
}
public CompletableFuture<Optional<RemovedMessage>> delete(UUID destinationUuid, Device destinationDevice, UUID guid,
@Nullable Long serverTimestamp) {
public CompletableFuture<Optional<RemovedMessage>> delete(final UUID destinationUuid,
final Device destinationDevice,
final UUID guid,
final long serverTimestamp) {
return messagesCache.remove(destinationUuid, destinationDevice.getId(), guid)
.thenComposeAsync(removed -> {
if (removed.isPresent()) {
return CompletableFuture.completedFuture(removed);
}
final CompletableFuture<Optional<MessageProtos.Envelope>> maybeDeletedEnvelope;
if (serverTimestamp == null) {
maybeDeletedEnvelope = messagesDynamoDb.deleteMessageByDestinationAndGuid(destinationUuid,
destinationDevice, guid);
} else {
maybeDeletedEnvelope = messagesDynamoDb.deleteMessage(destinationUuid, destinationDevice, guid,
serverTimestamp);
}
return maybeDeletedEnvelope.thenApply(maybeEnvelope -> maybeEnvelope.map(RemovedMessage::fromEnvelope));
}, messageDeletionExecutor);
.thenComposeAsync(removed -> removed
.map(_ -> CompletableFuture.completedFuture(removed))
.orElseGet(() -> messagesDynamoDb.deleteMessage(destinationUuid, destinationDevice, guid, serverTimestamp)
.thenApply(maybeEnvelope -> maybeEnvelope.map(RemovedMessage::fromEnvelope))
), messageDeletionExecutor);
}
/**

View File

@@ -58,7 +58,7 @@ public abstract class AbstractCommandWithDependencies extends EnvironmentCommand
} finally {
logger.info("Stopping command dependencies");
environment.lifecycle().getManagedObjects().forEach(managedObject -> {
environment.lifecycle().getManagedObjects().reversed().forEach(managedObject -> {
try {
managedObject.stop();
} catch (final Exception e) {

View File

@@ -7,10 +7,12 @@ package org.whispersystems.textsecuregcm.controllers;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
@@ -22,6 +24,9 @@ import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.signal.chat.calling.quality.SubmitCallQualitySurveyRequest;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
@@ -29,6 +34,7 @@ import org.whispersystems.textsecuregcm.metrics.CallQualitySurveyManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import org.whispersystems.textsecuregcm.util.TestRemoteAddressFilterProvider;
import java.util.List;
@ExtendWith(DropwizardExtensionsSupport.class)
class CallQualitySurveyControllerTest {
@@ -83,4 +89,21 @@ class CallQualitySurveyControllerTest {
verify(CALL_QUALITY_SURVEY_MANAGER, never()).submitCallQualitySurvey(any(), any(), any());
}
}
@Test
void submitCallQualitySurveyInvalidArgument() {
final SubmitCallQualitySurveyRequest request = SubmitCallQualitySurveyRequest.getDefaultInstance();
doThrow(new IllegalArgumentException())
.when(CALL_QUALITY_SURVEY_MANAGER).submitCallQualitySurvey(request, REMOTE_ADDRESS, USER_AGENT);
try (final Response response = RESOURCE_EXTENSION.getJerseyTest()
.target("/v1/call_quality_survey")
.request()
.header("User-Agent", USER_AGENT)
.put(Entity.entity(request.toByteArray(), MediaType.APPLICATION_OCTET_STREAM_TYPE))) {
assertEquals(422, response.getStatus());
}
}
}

View File

@@ -100,7 +100,6 @@ import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.spam.SpamChecker;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
@@ -108,13 +107,11 @@ import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.RemovedMessage;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.tests.util.MultiRecipientMessageHelper;
import org.whispersystems.textsecuregcm.tests.util.TestRecipient;
import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.SystemMapper;
@@ -165,7 +162,6 @@ class MessageControllerTest {
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);
@@ -192,7 +188,7 @@ class MessageControllerTest {
.addProvider(MultiRecipientMessageProvider.class)
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(
new MessageController(rateLimiters, cardinalityEstimator, messageSender, receiptSender, accountsManager,
new MessageController(rateLimiters, cardinalityEstimator, messageSender, accountsManager,
messagesManager, phoneNumberIdentifiers, pushNotificationManager, pushNotificationScheduler,
reportMessageManager, messageDeliveryScheduler, mock(ClientReleaseManager.class),
serverSecretParams, SpamChecker.noop(), new MessageMetrics(), mock(MessageDeliveryLoopMonitor.class),
@@ -269,7 +265,6 @@ class MessageControllerTest {
reset(
redisCommands,
messageSender,
receiptSender,
accountsManager,
messagesManager,
rateLimiters,
@@ -798,80 +793,6 @@ class MessageControllerTest {
assertThat("Unauthorized response", response.getStatus(), is(equalTo(401)));
}
@Test
void testDeleteMessages() {
long clientTimestamp = System.currentTimeMillis();
UUID sourceUuid = UUID.randomUUID();
UUID uuid1 = UUID.randomUUID();
final long serverTimestamp = 0;
when(messagesManager.delete(AuthHelper.VALID_UUID, AuthHelper.VALID_DEVICE, uuid1, null))
.thenReturn(
CompletableFutureTestUtil.almostCompletedFuture(Optional.of(
new RemovedMessage(Optional.of(new AciServiceIdentifier(sourceUuid)),
new AciServiceIdentifier(AuthHelper.VALID_UUID), uuid1, serverTimestamp, clientTimestamp,
Envelope.Type.CIPHERTEXT))));
UUID uuid2 = UUID.randomUUID();
when(messagesManager.delete(AuthHelper.VALID_UUID, AuthHelper.VALID_DEVICE, uuid2, null))
.thenReturn(
CompletableFutureTestUtil.almostCompletedFuture(Optional.of(
new RemovedMessage(Optional.of(new AciServiceIdentifier(sourceUuid)),
new AciServiceIdentifier(AuthHelper.VALID_UUID), uuid2, serverTimestamp, clientTimestamp,
Envelope.Type.SERVER_DELIVERY_RECEIPT))));
UUID uuid3 = UUID.randomUUID();
when(messagesManager.delete(AuthHelper.VALID_UUID, AuthHelper.VALID_DEVICE, uuid3, null))
.thenReturn(CompletableFutureTestUtil.almostCompletedFuture(Optional.empty()));
UUID uuid4 = UUID.randomUUID();
when(messagesManager.delete(AuthHelper.VALID_UUID, AuthHelper.VALID_DEVICE, uuid4, null))
.thenReturn(CompletableFuture.failedFuture(new RuntimeException("Oh No")));
try (final Response response = resources.getJerseyTest()
.target(String.format("/v1/messages/uuid/%s", uuid1))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.delete()) {
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verify(receiptSender).sendReceipt(eq(new AciServiceIdentifier(AuthHelper.VALID_UUID)), eq((byte) 1),
eq(new AciServiceIdentifier(sourceUuid)), eq(clientTimestamp));
}
try (final Response response = resources.getJerseyTest()
.target(String.format("/v1/messages/uuid/%s", uuid2))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.delete()) {
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verifyNoMoreInteractions(receiptSender);
}
try (final Response response = resources.getJerseyTest()
.target(String.format("/v1/messages/uuid/%s", uuid3))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.delete()) {
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verifyNoMoreInteractions(receiptSender);
}
try (final Response response = resources.getJerseyTest()
.target(String.format("/v1/messages/uuid/%s", uuid4))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.delete()) {
assertThat("Bad Response Code", response.getStatus(), is(equalTo(500)));
verifyNoMoreInteractions(receiptSender);
}
}
@Test
void testReportMessageByE164() {
final String senderNumber = "+12125550001";

View File

@@ -1,238 +0,0 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import jakarta.ws.rs.core.Response;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.InstanceOfAssertFactory;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.TestClock;
@ExtendWith(DropwizardExtensionsSupport.class)
class RemoteConfigControllerV1Test {
private static final RemoteConfigsManager remoteConfigsManager = mock(RemoteConfigsManager.class);
private static final long PINNED_EPOCH_SECONDS = 1701287216L;
private static final TestClock TEST_CLOCK = TestClock.pinned(Instant.ofEpochSecond(PINNED_EPOCH_SECONDS));
private static final ResourceExtension resources = ResourceExtension.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addProvider(new DeviceLimitExceededExceptionMapper())
.addResource(new RemoteConfigControllerV1(remoteConfigsManager, Map.of("maxGroupSize", "42"), TEST_CLOCK))
.build();
@BeforeEach
void setup() throws Exception {
when(remoteConfigsManager.getAll()).thenReturn(new LinkedList<>() {{
add(new RemoteConfig("android.stickers", 25, Set.of(AuthHelper.VALID_UUID_3, AuthHelper.INVALID_UUID), null,
null, null));
add(new RemoteConfig("ios.stickers", 50, Set.of(), null, null, null));
add(new RemoteConfig("always.true", 100, Set.of(), null, null, null));
add(new RemoteConfig("only.special", 0, Set.of(AuthHelper.VALID_UUID), null, null, null));
add(new RemoteConfig("value.always.true", 100, Set.of(), "foo", "bar", null));
add(new RemoteConfig("value.only.special", 0, Set.of(AuthHelper.VALID_UUID), "abc", "xyz", null));
add(new RemoteConfig("value.always.false", 0, Set.of(), "red", "green", null));
add(new RemoteConfig("linked.config.0", 50, Set.of(), null, null, null));
add(new RemoteConfig("linked.config.1", 50, Set.of(), null, null, "linked.config.0"));
add(new RemoteConfig("unlinked.config", 50, Set.of(), null, null, null));
}});
}
@AfterEach
void teardown() {
reset(remoteConfigsManager);
}
@Test
void testRetrieveConfig() {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(UserRemoteConfigList.class);
verify(remoteConfigsManager, times(1)).getAll();
assertThat(configuration.getConfig()).hasSize(11);
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("xyz");
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
}
@Test
void testServerEpochTime() {
Object serverEpochTime = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(Map.class)
.get("serverEpochTime");
assertThat(serverEpochTime).asInstanceOf(new InstanceOfAssertFactory<>(Number.class, Assertions::assertThat))
.extracting(Number::longValue)
.isEqualTo(PINNED_EPOCH_SECONDS);
}
@Test
void testRetrieveConfigNotSpecial() {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))
.get(UserRemoteConfigList.class);
verify(remoteConfigsManager, times(1)).getAll();
assertThat(configuration.getConfig()).hasSize(11);
assertThat(configuration.getConfig().get(0).getName()).isEqualTo("android.stickers");
assertThat(configuration.getConfig().get(1).getName()).isEqualTo("ios.stickers");
assertThat(configuration.getConfig().get(2).getName()).isEqualTo("always.true");
assertThat(configuration.getConfig().get(2).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(3).getName()).isEqualTo("only.special");
assertThat(configuration.getConfig().get(3).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(2).getValue()).isNull();
assertThat(configuration.getConfig().get(4).getName()).isEqualTo("value.always.true");
assertThat(configuration.getConfig().get(4).isEnabled()).isEqualTo(true);
assertThat(configuration.getConfig().get(4).getValue()).isEqualTo("bar");
assertThat(configuration.getConfig().get(5).getName()).isEqualTo("value.only.special");
assertThat(configuration.getConfig().get(5).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(5).getValue()).isEqualTo("abc");
assertThat(configuration.getConfig().get(6).getName()).isEqualTo("value.always.false");
assertThat(configuration.getConfig().get(6).isEnabled()).isEqualTo(false);
assertThat(configuration.getConfig().get(6).getValue()).isEqualTo("red");
assertThat(configuration.getConfig().get(7).getName()).isEqualTo("linked.config.0");
assertThat(configuration.getConfig().get(8).getName()).isEqualTo("linked.config.1");
assertThat(configuration.getConfig().get(9).getName()).isEqualTo("unlinked.config");
assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize");
}
@Test
void testHashKeyLinkedConfigs() {
boolean allUnlinkedConfigsMatched = true;
for (AuthHelper.TestAccount testAccount : AuthHelper.TEST_ACCOUNTS) {
UserRemoteConfigList configuration = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", testAccount.getAuthHeader())
.get(UserRemoteConfigList.class);
assertThat(configuration.getConfig()).hasSize(11);
final UserRemoteConfig linkedConfig0 = configuration.getConfig().get(7);
assertThat(linkedConfig0.getName()).isEqualTo("linked.config.0");
final UserRemoteConfig linkedConfig1 = configuration.getConfig().get(8);
assertThat(linkedConfig1.getName()).isEqualTo("linked.config.1");
final UserRemoteConfig unlinkedConfig = configuration.getConfig().get(9);
assertThat(unlinkedConfig.getName()).isEqualTo("unlinked.config");
assertThat(linkedConfig0.isEnabled() == linkedConfig1.isEnabled()).isTrue();
allUnlinkedConfigsMatched &= (linkedConfig0.isEnabled() == unlinkedConfig.isEnabled());
}
assertThat(allUnlinkedConfigsMatched).isFalse();
}
@Test
void testRetrieveConfigUnauthorized() {
Response response = resources.getJerseyTest()
.target("/v1/config/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
.get();
assertThat(response.getStatus()).isEqualTo(401);
verifyNoMoreInteractions(remoteConfigsManager);
}
@Test
void testMath() throws NoSuchAlgorithmException {
List<RemoteConfig> remoteConfigList = remoteConfigsManager.getAll();
Map<String, Integer> enabledMap = new HashMap<>();
MessageDigest digest = MessageDigest.getInstance("SHA256");
int iterations = 100000;
Random random = new Random(9424242L); // the seed value doesn't matter so much as it's constant to make the test not flaky
for (int i=0;i<iterations;i++) {
for (RemoteConfig config : remoteConfigList) {
int count = enabledMap.getOrDefault(config.getName(), 0);
if (RemoteConfigControllerV1.isInBucket(digest, AuthHelper.getRandomUUID(random), config.getName().getBytes(), config.getPercentage(), new HashSet<>())) {
count++;
}
enabledMap.put(config.getName(), count);
}
}
for (RemoteConfig config : remoteConfigList) {
double targetNumber = iterations * (config.getPercentage() / 100.0);
double variance = targetNumber * 0.01;
assertThat(enabledMap.get(config.getName())).isBetween((int) (targetNumber - variance),
(int) (targetNumber + variance));
}
}
}

View File

@@ -13,6 +13,7 @@ import static org.mockito.Mockito.when;
import com.google.common.net.InetAddresses;
import java.time.Duration;
import io.grpc.Status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
@@ -67,4 +68,16 @@ class CallQualitySurveyGrpcServiceTest extends SimpleBaseGrpcTest<CallQualitySur
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
() -> unauthenticatedServiceStub().submitCallQualitySurvey(SubmitCallQualitySurveyRequest.getDefaultInstance()));
}
@Test
void submitCallQualitySurveyInvalidArgument() {
final SubmitCallQualitySurveyRequest request = SubmitCallQualitySurveyRequest.getDefaultInstance();
doThrow(new IllegalArgumentException())
.when(callQualitySurveyManager).submitCallQualitySurvey(request, REMOTE_ADDRESS, USER_AGENT);
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> unauthenticatedServiceStub().submitCallQualitySurvey(request));
}
}

View File

@@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -27,6 +28,11 @@ import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.platform.commons.util.StringUtils;
import org.mockito.ArgumentCaptor;
import org.signal.calling.survey.CallQualitySurveyResponsePubSubMessage;
import org.signal.chat.calling.quality.SubmitCallQualitySurveyRequest;
@@ -140,4 +146,36 @@ class CallQualitySurveyManagerTest {
assertEquals(videoSendPacketLossFraction, callQualitySurveyResponsePubSubMessage.getVideoSendPacketLossFraction());
assertArrayEquals(telemetryBytes, callQualitySurveyResponsePubSubMessage.getCallTelemetry().toByteArray());
}
@ParameterizedTest
@MethodSource
void validateRequest(final SubmitCallQualitySurveyRequest request, final boolean expectValid) {
final Executable validateRequest = () -> CallQualitySurveyManager.validateRequest(request);
if (expectValid) {
assertDoesNotThrow(validateRequest);
} else {
final IllegalArgumentException illegalArgumentException =
assertThrows(IllegalArgumentException.class, validateRequest);
assertTrue(StringUtils.isNotBlank(illegalArgumentException.getMessage()));
}
}
private static List<Arguments> validateRequest() {
final SubmitCallQualitySurveyRequest validRequest = SubmitCallQualitySurveyRequest.newBuilder()
.setStartTimestamp(Instant.now().toEpochMilli())
.setEndTimestamp(Instant.now().plusSeconds(60).toEpochMilli())
.setCallType("test")
.setCallEndReason("test")
.build();
return List.of(
Arguments.argumentSet("Valid survey response", validRequest, true),
Arguments.argumentSet("No start timestamp", validRequest.toBuilder().clearStartTimestamp().build(), false),
Arguments.argumentSet("No end timestamp", validRequest.toBuilder().clearEndTimestamp().build(), false),
Arguments.argumentSet("No call type", validRequest.toBuilder().clearCallType().build(), false),
Arguments.argumentSet("No call end reason", validRequest.toBuilder().clearCallEndReason().build(), false)
);
}
}

View File

@@ -8,8 +8,8 @@ package org.whispersystems.textsecuregcm.storage;
import java.util.Collections;
import java.util.List;
import org.whispersystems.textsecuregcm.backup.BackupsDb;
import org.whispersystems.textsecuregcm.scheduler.JobScheduler;
import org.whispersystems.textsecuregcm.experiment.PushNotificationExperimentSamples;
import org.whispersystems.textsecuregcm.scheduler.JobScheduler;
import org.whispersystems.textsecuregcm.storage.devicecheck.AppleDeviceChecks;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex;
@@ -246,20 +246,8 @@ public final class DynamoDbExtensionSchema {
MessagesDynamoDb.KEY_SORT,
List.of(
AttributeDefinition.builder().attributeName(MessagesDynamoDb.KEY_PARTITION).attributeType(ScalarAttributeType.B).build(),
AttributeDefinition.builder().attributeName(MessagesDynamoDb.KEY_SORT).attributeType(ScalarAttributeType.B).build(),
AttributeDefinition.builder().attributeName(MessagesDynamoDb.LOCAL_INDEX_MESSAGE_UUID_KEY_SORT)
.attributeType(ScalarAttributeType.B).build()),
List.of(),
List.of(LocalSecondaryIndex.builder()
.indexName(MessagesDynamoDb.LOCAL_INDEX_MESSAGE_UUID_NAME)
.keySchema(
KeySchemaElement.builder().attributeName(MessagesDynamoDb.KEY_PARTITION).keyType(KeyType.HASH).build(),
KeySchemaElement.builder()
.attributeName(MessagesDynamoDb.LOCAL_INDEX_MESSAGE_UUID_KEY_SORT)
.keyType(KeyType.RANGE)
.build())
.projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build())
.build())),
AttributeDefinition.builder().attributeName(MessagesDynamoDb.KEY_SORT).attributeType(ScalarAttributeType.B).build()),
List.of(), List.of()),
ONETIME_DONATIONS("onetime_donations_test",
OneTimeDonationsManager.KEY_PAYMENT_ID,

View File

@@ -11,9 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyByte;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNotNull;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
@@ -346,15 +346,16 @@ class MessagePersisterTest {
.thenReturn(Flux.concat(
Flux.fromIterable(persistedMessages),
Flux.fromIterable(cachedMessages)));
when(messagesManager.delete(any(), any(), any(), any()))
when(messagesManager.delete(any(), any(), any(), anyLong()))
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
assertTimeoutPreemptively(Duration.ofSeconds(10), () ->
messagePersister.persistNextQueues(Clock.systemUTC().instant()));
verify(messagesManager, times(expectedClearedGuids.size()))
.delete(eq(DESTINATION_ACCOUNT_UUID), eq(primary), argThat(expectedClearedGuids::contains), isNotNull());
verify(messagesManager, never()).delete(any(), any(), argThat(guid -> !expectedClearedGuids.contains(guid)), any());
.delete(eq(DESTINATION_ACCOUNT_UUID), eq(primary), argThat(expectedClearedGuids::contains), anyLong());
verify(messagesManager, never())
.delete(any(), any(), argThat(guid -> !expectedClearedGuids.contains(guid)), anyLong());
final List<String> queuesToPersist = messagesCache.getQueuesToPersist(SlotHash.getSlot(queueName),
Clock.systemUTC().instant(), 1);

View File

@@ -12,7 +12,6 @@ import com.google.protobuf.ByteString;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@@ -188,47 +187,6 @@ class MessagesDynamoDbTest {
.verify();
}
@Test
void testDeleteMessageByDestinationAndGuid() throws Exception {
final UUID destinationUuid = UUID.randomUUID();
final UUID secondDestinationUuid = UUID.randomUUID();
final Device primary = DevicesHelper.createDevice((byte) 1);
final Device device2 = DevicesHelper.createDevice((byte) 2);
messagesDynamoDb.store(List.of(MESSAGE1), destinationUuid, primary);
messagesDynamoDb.store(List.of(MESSAGE2), secondDestinationUuid, primary);
messagesDynamoDb.store(List.of(MESSAGE3), destinationUuid, device2);
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1)
.element(0).isEqualTo(MESSAGE1);
assertThat(load(destinationUuid, device2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull()
.hasSize(1)
.element(0).isEqualTo(MESSAGE3);
assertThat(load(secondDestinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull()
.hasSize(1).element(0).isEqualTo(MESSAGE2);
final Optional<MessageProtos.Envelope> deletedMessage = messagesDynamoDb.deleteMessageByDestinationAndGuid(
secondDestinationUuid, primary,
UUID.fromString(MESSAGE2.getServerGuid())).get(5, TimeUnit.SECONDS);
assertThat(deletedMessage).isPresent();
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull().hasSize(1)
.element(0).isEqualTo(MESSAGE1);
assertThat(load(destinationUuid, device2, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull()
.hasSize(1)
.element(0).isEqualTo(MESSAGE3);
assertThat(load(secondDestinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE)).isNotNull()
.isEmpty();
final Optional<MessageProtos.Envelope> alreadyDeletedMessage = messagesDynamoDb.deleteMessageByDestinationAndGuid(
secondDestinationUuid, primary,
UUID.fromString(MESSAGE2.getServerGuid())).get(5, TimeUnit.SECONDS);
assertThat(alreadyDeletedMessage).isNotPresent();
}
@Test
void testDeleteSingleMessage() throws Exception {
final UUID destinationUuid = UUID.randomUUID();
@@ -274,19 +232,14 @@ class MessagesDynamoDbTest {
final Device primary = DevicesHelper.createDevice((byte) 1);
primary.setCreated(System.currentTimeMillis());
messagesDynamoDb.store(List.of(MESSAGE1, MESSAGE2, MESSAGE3), destinationUuid, primary);
messagesDynamoDb.store(List.of(MESSAGE1, MESSAGE2), destinationUuid, primary);
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE))
.as("load should return all messages stored").containsOnly(MESSAGE1, MESSAGE2, MESSAGE3);
.as("load should return all messages stored").containsOnly(MESSAGE1, MESSAGE2);
messagesDynamoDb.deleteMessageByDestinationAndGuid(destinationUuid, primary, UUID.fromString(MESSAGE1.getServerGuid()))
messagesDynamoDb.deleteMessage(destinationUuid, primary, UUID.fromString(MESSAGE1.getServerGuid()), MESSAGE1.getServerTimestamp())
.get(1, TimeUnit.SECONDS);
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE))
.as("deleting message by guid should work").containsExactly(MESSAGE3, MESSAGE2);
messagesDynamoDb.deleteMessage(destinationUuid, primary, UUID.fromString(MESSAGE2.getServerGuid()), MESSAGE2.getServerTimestamp())
.get(1, TimeUnit.SECONDS);
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE))
.as("deleting message by guid and timestamp should work").containsExactly(MESSAGE3);
.as("deleting message by guid and timestamp should work").containsExactly(MESSAGE2);
primary.setCreated(primary.getCreated() + 1000);
assertThat(load(destinationUuid, primary, MessagesDynamoDb.RESULT_SET_CHUNK_SIZE))