diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/EditMessageSyncProcessorTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/EditMessageSyncProcessorTest.kt index 19c1ffffb3..31920e51d9 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/EditMessageSyncProcessorTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/EditMessageSyncProcessorTest.kt @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.testing.MessageContentFuzzer import org.thoughtcrime.securesms.testing.SignalActivityRule import org.thoughtcrime.securesms.testing.assertIs -import org.thoughtcrime.securesms.util.MessageTableUtils +import org.thoughtcrime.securesms.util.MessageTableTestUtils import org.whispersystems.signalservice.internal.push.SignalServiceProtos import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage import kotlin.time.Duration.Companion.seconds @@ -238,7 +238,7 @@ class EditMessageSyncProcessorTest { else -> cursor.getString(index) } if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) { - data = MessageTableUtils.typeColumnToString(cursor.getLong(index)) + data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index)) } column to data diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt index ceb13397b2..ec4dadc7ca 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTestV2.kt @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.testing.InMemoryLogger import org.thoughtcrime.securesms.testing.MessageContentFuzzer import org.thoughtcrime.securesms.testing.SignalActivityRule import org.thoughtcrime.securesms.testing.assertIs -import org.thoughtcrime.securesms.util.MessageTableUtils +import org.thoughtcrime.securesms.util.MessageTableTestUtils import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata import org.whispersystems.signalservice.api.messages.SignalServiceContent import org.whispersystems.signalservice.api.messages.SignalServiceMetadata @@ -278,7 +278,7 @@ class MessageContentProcessorTestV2 { else -> cursor.getString(index) } if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) { - data = MessageTableUtils.typeColumnToString(cursor.getLong(index)) + data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index)) } column to data diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorV2__recipientStatusTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorV2__recipientStatusTest.kt new file mode 100644 index 0000000000..121cc3a0cd --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorV2__recipientStatusTest.kt @@ -0,0 +1,84 @@ +package org.thoughtcrime.securesms.messages + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.thoughtcrime.securesms.database.GroupReceiptTable +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.toProtoByteString +import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith +import org.thoughtcrime.securesms.testing.GroupTestingUtils +import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember +import org.thoughtcrime.securesms.testing.MessageContentFuzzer +import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.testing.assertIs +import org.thoughtcrime.securesms.util.MessageTableTestUtils +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2 + +@Suppress("ClassName") +@RunWith(AndroidJUnit4::class) +class MessageContentProcessorV2__recipientStatusTest { + + @get:Rule + val harness = SignalActivityRule() + + private lateinit var processorV2: MessageContentProcessorV2 + private var envelopeTimestamp: Long = 0 + + @Before + fun setup() { + processorV2 = MessageContentProcessorV2(harness.context) + envelopeTimestamp = System.currentTimeMillis() + } + + /** + * Process sync group sent text transcript with partial send and then process second sync with recipient update + * flag set to true with the rest of the send completed. + */ + @Test + fun syncGroupSentTextMessageWithRecipientUpdateFollowup() { + val (groupId, masterKey, groupRecipientId) = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), harness.others[0].asMember(), harness.others[1].asMember()) + val groupContextV2 = GroupContextV2.newBuilder().setRevision(0).setMasterKey(masterKey.serialize().toProtoByteString()).build() + + val initialTextMessage = DataMessage.newBuilder().buildWith { + body = MessageContentFuzzer.string() + groupV2 = groupContextV2 + timestamp = envelopeTimestamp + } + + processorV2.process( + envelope = MessageContentFuzzer.envelope(envelopeTimestamp), + content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])), + metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId), + serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp) + ) + + val threadId = SignalDatabase.threads.getThreadIdFor(groupRecipientId)!! + val firstSyncMessages = MessageTableTestUtils.getMessages(threadId) + val firstMessageId = firstSyncMessages[0].id + val firstReceiptInfo = SignalDatabase.groupReceipts.getGroupReceiptInfo(firstMessageId) + + processorV2.process( + envelope = MessageContentFuzzer.envelope(envelopeTimestamp), + content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0], harness.others[1]), recipientUpdate = true), + metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId), + serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp) + ) + + val secondSyncMessages = MessageTableTestUtils.getMessages(threadId) + val secondReceiptInfo = SignalDatabase.groupReceipts.getGroupReceiptInfo(firstMessageId) + + firstSyncMessages.size assertIs 1 + firstSyncMessages[0].body assertIs initialTextMessage.body + firstReceiptInfo.first { it.recipientId == harness.others[0] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED + firstReceiptInfo.first { it.recipientId == harness.others[1] }.status assertIs GroupReceiptTable.STATUS_UNKNOWN + + secondSyncMessages.size assertIs 1 + secondSyncMessages[0].body assertIs initialTextMessage.body + secondReceiptInfo.first { it.recipientId == harness.others[0] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED + secondReceiptInfo.first { it.recipientId == harness.others[1] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt new file mode 100644 index 0000000000..9235eeb72d --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms.testing + +import org.signal.libsignal.zkgroup.groups.GroupMasterKey +import org.signal.storageservice.protos.groups.Member +import org.signal.storageservice.protos.groups.local.DecryptedGroup +import org.signal.storageservice.protos.groups.local.DecryptedMember +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.whispersystems.signalservice.api.push.ServiceId +import kotlin.random.Random + +/** + * Helper methods for creating groups for message processing tests et al. + */ +object GroupTestingUtils { + fun member(serviceId: ServiceId, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember { + return DecryptedMember.newBuilder() + .setUuid(serviceId.toByteString()) + .setJoinedAtRevision(revision) + .setRole(role) + .build() + } + + fun insertGroup(revision: Int = 0, vararg members: DecryptedMember): TestGroupInfo { + val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE)) + val decryptedGroupState = DecryptedGroup.newBuilder() + .addAllMembers(members.toList()) + .setRevision(revision) + .setTitle(MessageContentFuzzer.string()) + .build() + + val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState) + val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId) + SignalDatabase.recipients.setProfileSharing(groupRecipientId, true) + + return TestGroupInfo(groupId, groupMasterKey, groupRecipientId) + } + + fun RecipientId.asMember(): DecryptedMember { + return Recipient.resolved(this).asMember() + } + + fun Recipient.asMember(): DecryptedMember { + return member(serviceId = requireServiceId()) + } + + data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt index f471145476..16411eb745 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.testing import com.google.protobuf.ByteString import org.thoughtcrime.securesms.database.model.toProtoByteString +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith import org.thoughtcrime.securesms.messages.TestMessage import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -12,6 +14,8 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Attach import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2 +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage import java.util.UUID import kotlin.random.Random import kotlin.random.nextInt @@ -41,13 +45,13 @@ object MessageContentFuzzer { /** * Create metadata to match an [Envelope]. */ - fun envelopeMetadata(source: RecipientId, destination: RecipientId): EnvelopeMetadata { + fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata { return EnvelopeMetadata( sourceServiceId = Recipient.resolved(source).requireServiceId(), sourceE164 = null, sourceDeviceId = 1, sealedSender = true, - groupId = null, + groupId = groupId?.decodedId, destinationServiceId = Recipient.resolved(destination).requireServiceId() ) } @@ -57,30 +61,60 @@ object MessageContentFuzzer { * - An expire timer value * - Bold style body ranges */ - fun fuzzTextMessage(): Content { + fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content { return Content.newBuilder() .setDataMessage( - DataMessage.newBuilder().run { + DataMessage.newBuilder().buildWith { body = string() if (random.nextBoolean()) { expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt()) } if (random.nextBoolean()) { addBodyRanges( - SignalServiceProtos.BodyRange.newBuilder().run { + SignalServiceProtos.BodyRange.newBuilder().buildWith { start = 0 length = 1 style = SignalServiceProtos.BodyRange.Style.BOLD - build() } ) } - build() + if (groupContextV2 != null) { + groupV2 = groupContextV2 + } } ) .build() } + /** + * Create a sync sent text message for the given [DataMessage]. + */ + fun syncSentTextMessage( + textMessage: DataMessage, + deliveredTo: List, + recipientUpdate: Boolean = false + ): Content { + return Content + .newBuilder() + .setSyncMessage( + SyncMessage.newBuilder().buildWith { + sent = SyncMessage.Sent.newBuilder().buildWith { + timestamp = textMessage.timestamp + message = textMessage + isRecipientUpdate = recipientUpdate + addAllUnidentifiedStatus( + deliveredTo.map { + SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder().buildWith { + destinationUuid = Recipient.resolved(it).requireServiceId().toString() + unidentified = true + } + } + ) + } + } + ).build() + } + /** * Create a random media message that may be: * - A text body @@ -91,7 +125,7 @@ object MessageContentFuzzer { fun fuzzMediaMessageWithBody(quoteAble: List = emptyList()): Content { return Content.newBuilder() .setDataMessage( - DataMessage.newBuilder().run { + DataMessage.newBuilder().buildWith { if (random.nextBoolean()) { body = string() } @@ -99,24 +133,22 @@ object MessageContentFuzzer { if (random.nextBoolean() && quoteAble.isNotEmpty()) { body = string() val quoted = quoteAble.random(random) - quote = DataMessage.Quote.newBuilder().run { + quote = DataMessage.Quote.newBuilder().buildWith { id = quoted.envelope.timestamp authorUuid = quoted.metadata.sourceServiceId.toString() text = quoted.content.dataMessage.body addAllAttachments(quoted.content.dataMessage.attachmentsList) addAllBodyRanges(quoted.content.dataMessage.bodyRangesList) type = DataMessage.Quote.Type.NORMAL - build() } } if (random.nextFloat() < 0.1 && quoteAble.isNotEmpty()) { val quoted = quoteAble.random(random) - quote = DataMessage.Quote.newBuilder().run { + quote = DataMessage.Quote.newBuilder().buildWith { id = random.nextLong(quoted.envelope.timestamp - 1000000, quoted.envelope.timestamp) authorUuid = quoted.metadata.sourceServiceId.toString() text = quoted.content.dataMessage.body - build() } } @@ -124,8 +156,6 @@ object MessageContentFuzzer { val total = random.nextInt(1, 2) (0..total).forEach { _ -> addAttachments(attachmentPointer()) } } - - build() } ) .build() @@ -138,19 +168,16 @@ object MessageContentFuzzer { fun fuzzMediaMessageNoContent(previousMessages: List = emptyList()): Content { return Content.newBuilder() .setDataMessage( - DataMessage.newBuilder().run { + DataMessage.newBuilder().buildWith { if (random.nextFloat() < 0.25) { val reactTo = previousMessages.random(random) - reaction = DataMessage.Reaction.newBuilder().run { + reaction = DataMessage.Reaction.newBuilder().buildWith { emoji = emojis.random(random) remove = false targetAuthorUuid = reactTo.metadata.sourceServiceId.toString() targetSentTimestamp = reactTo.envelope.timestamp - build() } } - - build() } ).build() } @@ -162,18 +189,16 @@ object MessageContentFuzzer { fun fuzzMediaMessageNoText(previousMessages: List = emptyList()): Content { return Content.newBuilder() .setDataMessage( - DataMessage.newBuilder().run { + DataMessage.newBuilder().buildWith { if (random.nextFloat() < 0.9) { - sticker = DataMessage.Sticker.newBuilder().run { + sticker = DataMessage.Sticker.newBuilder().buildWith { packId = byteString(length = 24) packKey = byteString(length = 128) stickerId = random.nextInt() data = attachmentPointer() emoji = emojis.random(random) - build() } } - build() } ).build() } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/util/MessageTableUtils.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/util/MessageTableTestUtils.kt similarity index 91% rename from app/src/androidTest/java/org/thoughtcrime/securesms/util/MessageTableUtils.kt rename to app/src/androidTest/java/org/thoughtcrime/securesms/util/MessageTableTestUtils.kt index c173e1bc6e..70dba1df2d 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/util/MessageTableUtils.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/util/MessageTableTestUtils.kt @@ -1,8 +1,21 @@ package org.thoughtcrime.securesms.util +import org.thoughtcrime.securesms.database.MessageTable import org.thoughtcrime.securesms.database.MessageTypes +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.MessageRecord + +/** + * Helper methods for interacting with [MessageTable] in tests. + */ +object MessageTableTestUtils { + + fun getMessages(threadId: Long): List { + return MessageTable.mmsReaderFor(SignalDatabase.messages.getConversation(threadId)).use { + it.toList() + } + } -object MessageTableUtils { fun typeColumnToString(type: Long): String { return """ isOutgoingMessageType:${MessageTypes.isOutgoingMessageType(type)} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SignalServiceProtoUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SignalServiceProtoUtil.kt index 97f5fbce46..e27b1b8f91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SignalServiceProtoUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SignalServiceProtoUtil.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.messages import com.google.protobuf.ByteString +import com.google.protobuf.GeneratedMessageLite import org.signal.core.util.orNull import org.signal.libsignal.protocol.message.DecryptionErrorMessage import org.signal.libsignal.zkgroup.groups.GroupMasterKey @@ -177,4 +178,10 @@ object SignalServiceProtoUtil { fun Long.toMobileCoinMoney(): Money { return Money.picoMobileCoin(this) } + + @Suppress("UNCHECKED_CAST") + inline fun , BuilderType : GeneratedMessageLite.Builder> GeneratedMessageLite.Builder.buildWith(block: BuilderType.() -> Unit): MessageType { + block(this as BuilderType) + return build() + } }