From 874f808d56250dea9a88eccce78ca7fb4f373b53 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 18 Mar 2024 09:47:41 -0400 Subject: [PATCH] Add process read sync tests. --- ...geContentProcessor__recipientStatusTest.kt | 4 +- .../SyncMessageProcessorTest_readSyncs.kt | 225 ++++++++++++++++++ .../securesms/testing/GroupTestingUtils.kt | 7 +- .../securesms/testing/MessageContentFuzzer.kt | 65 +++-- .../securesms/database/MessageTable.kt | 15 +- .../messages/SyncMessageProcessor.kt | 7 +- 6 files changed, 290 insertions(+), 33 deletions(-) create mode 100644 app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_readSyncs.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__recipientStatusTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__recipientStatusTest.kt index 606631dd18..2e7f38ba43 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__recipientStatusTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__recipientStatusTest.kt @@ -52,7 +52,7 @@ class MessageContentProcessor__recipientStatusTest { processor.process( envelope = MessageContentFuzzer.envelope(envelopeTimestamp), content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])), - metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId), + metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId), serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp) ) @@ -64,7 +64,7 @@ class MessageContentProcessor__recipientStatusTest { processor.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), + metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId), serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp) ) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_readSyncs.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_readSyncs.kt new file mode 100644 index 0000000000..36a752235c --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_readSyncs.kt @@ -0,0 +1,225 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.messages + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.unmockkStatic +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.jobs.ThreadUpdateJob +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testing.GroupTestingUtils +import org.thoughtcrime.securesms.testing.MessageContentFuzzer +import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.testing.assertIs +import java.util.UUID + +@Suppress("ClassName") +@RunWith(AndroidJUnit4::class) +class SyncMessageProcessorTest_readSyncs { + + @get:Rule + val harness = SignalActivityRule(createGroup = true) + + private lateinit var alice: RecipientId + private lateinit var bob: RecipientId + private lateinit var group: GroupTestingUtils.TestGroupInfo + private lateinit var processor: MessageContentProcessor + + @Before + fun setUp() { + alice = harness.others[0] + bob = harness.others[1] + group = harness.group!! + + processor = MessageContentProcessor(harness.context) + + val threadIdSlot = slot() + mockkStatic(ThreadUpdateJob::class) + every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers { + SignalDatabase.threads.update(threadIdSlot.captured, false) + } + } + + @After + fun tearDown() { + unmockkStatic(ThreadUpdateJob::class) + } + + @Test + fun handleSynchronizeReadMessage() { + val messageHelper = MessageHelper() + + val message1Timestamp = messageHelper.incomingText().timestamp + val message2Timestamp = messageHelper.incomingText().timestamp + + val threadId = SignalDatabase.threads.getThreadIdFor(alice)!! + var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!! + threadRecord.unreadCount assertIs 2 + + messageHelper.syncReadMessage(alice to message1Timestamp, alice to message2Timestamp) + + threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!! + threadRecord.unreadCount assertIs 0 + } + + @Test + fun handleSynchronizeReadMessageMissingTimestamp() { + val messageHelper = MessageHelper() + + messageHelper.incomingText().timestamp + val message2Timestamp = messageHelper.incomingText().timestamp + + val threadId = SignalDatabase.threads.getThreadIdFor(alice)!! + var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!! + threadRecord.unreadCount assertIs 2 + + messageHelper.syncReadMessage(alice to message2Timestamp) + + threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!! + threadRecord.unreadCount assertIs 0 + } + + @Test + fun handleSynchronizeReadWithEdits() { + val messageHelper = MessageHelper() + + val message1Timestamp = messageHelper.incomingText().timestamp + messageHelper.syncReadMessage(alice to message1Timestamp) + + val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp + val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp + + val message2Timestamp = messageHelper.incomingMedia().timestamp + + val threadId = SignalDatabase.threads.getThreadIdFor(alice)!! + var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!! + threadRecord.unreadCount assertIs 2 + + messageHelper.syncReadMessage(alice to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2) + + threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!! + threadRecord.unreadCount assertIs 0 + } + + @Test + fun handleSynchronizeReadWithEditsInGroup() { + val messageHelper = MessageHelper() + + val message1Timestamp = messageHelper.incomingText(sender = alice, destination = group.recipientId).timestamp + + messageHelper.syncReadMessage(alice to message1Timestamp) + + val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = alice, destination = group.recipientId).timestamp + val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = alice, destination = group.recipientId).timestamp + + val message2Timestamp = messageHelper.incomingMedia(sender = bob, destination = group.recipientId).timestamp + + val threadId = SignalDatabase.threads.getThreadIdFor(group.recipientId)!! + var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!! + threadRecord.unreadCount assertIs 2 + + messageHelper.syncReadMessage(bob to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2) + + threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!! + threadRecord.unreadCount assertIs 0 + } + + private inner class MessageHelper(var startTime: Long = System.currentTimeMillis()) { + + fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData { + startTime += 1000 + + val messageData = MessageData(timestamp = startTime) + + processor.process( + envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid), + content = MessageContentFuzzer.fuzzTextMessage( + sentTimestamp = messageData.timestamp, + groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null + ), + metadata = MessageContentFuzzer.envelopeMetadata( + source = sender, + destination = harness.self.id, + groupId = if (destination == group.recipientId) group.groupId else null + ), + serverDeliveredTimestamp = messageData.timestamp + 10 + ) + + return messageData + } + + fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData { + startTime += 1000 + + val messageData = MessageData(timestamp = startTime) + + processor.process( + envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid), + content = MessageContentFuzzer.fuzzStickerMediaMessage( + sentTimestamp = messageData.timestamp, + groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null + ), + metadata = MessageContentFuzzer.envelopeMetadata( + source = sender, + destination = harness.self.id, + groupId = if (destination == group.recipientId) group.groupId else null + ), + serverDeliveredTimestamp = messageData.timestamp + 10 + ) + + return messageData + } + + fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData { + startTime += 1000 + + val messageData = MessageData(timestamp = startTime) + + processor.process( + envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid), + content = MessageContentFuzzer.editTextMessage( + targetTimestamp = targetTimestamp, + editedDataMessage = MessageContentFuzzer.fuzzTextMessage( + sentTimestamp = messageData.timestamp, + groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null + ).dataMessage!! + ), + metadata = MessageContentFuzzer.envelopeMetadata( + source = sender, + destination = harness.self.id, + groupId = if (destination == group.recipientId) group.groupId else null + ), + serverDeliveredTimestamp = messageData.timestamp + 10 + ) + + return messageData + } + + fun syncReadMessage(vararg reads: Pair): MessageData { + startTime += 1000 + val messageData = MessageData(timestamp = startTime) + + processor.process( + envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid), + content = MessageContentFuzzer.syncReadsMessage(reads.toList()), + metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2), + serverDeliveredTimestamp = messageData.timestamp + 10 + ) + + return messageData + } + } + + private data class MessageData(val serverGuid: UUID = UUID.randomUUID(), val timestamp: Long) +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt index 284899713b..0c0d366194 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/GroupTestingUtils.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.testing +import okio.ByteString.Companion.toByteString import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.signal.storageservice.protos.groups.Member import org.signal.storageservice.protos.groups.local.DecryptedGroup @@ -9,6 +10,7 @@ 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.ACI +import org.whispersystems.signalservice.internal.push.GroupContextV2 import kotlin.random.Random /** @@ -46,5 +48,8 @@ object GroupTestingUtils { return member(aci = requireAci()) } - data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) + data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) { + val groupV2Context: GroupContextV2 + get() = GroupContextV2(masterKey = masterKey.serialize().toByteString(), revision = 0) + } } 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 bc2bc11dd7..38a366315b 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MessageContentFuzzer.kt @@ -12,6 +12,7 @@ import org.whispersystems.signalservice.internal.push.AttachmentPointer import org.whispersystems.signalservice.internal.push.BodyRange import org.whispersystems.signalservice.internal.push.Content import org.whispersystems.signalservice.internal.push.DataMessage +import org.whispersystems.signalservice.internal.push.EditMessage import org.whispersystems.signalservice.internal.push.Envelope import org.whispersystems.signalservice.internal.push.GroupContextV2 import org.whispersystems.signalservice.internal.push.SyncMessage @@ -33,22 +34,22 @@ object MessageContentFuzzer { /** * Create an [Envelope]. */ - fun envelope(timestamp: Long): Envelope { + fun envelope(timestamp: Long, serverGuid: UUID = UUID.randomUUID()): Envelope { return Envelope.Builder() .timestamp(timestamp) .serverTimestamp(timestamp + 5) - .serverGuid(UUID.randomUUID().toString()) + .serverGuid(serverGuid.toString()) .build() } /** * Create metadata to match an [Envelope]. */ - fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata { + fun envelopeMetadata(source: RecipientId, destination: RecipientId, sourceDeviceId: Int = 1, groupId: GroupId.V2? = null): EnvelopeMetadata { return EnvelopeMetadata( sourceServiceId = Recipient.resolved(source).requireServiceId(), sourceE164 = null, - sourceDeviceId = 1, + sourceDeviceId = sourceDeviceId, sealedSender = true, groupId = groupId?.decodedId, destinationServiceId = Recipient.resolved(destination).requireServiceId() @@ -60,10 +61,11 @@ object MessageContentFuzzer { * - An expire timer value * - Bold style body ranges */ - fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content { + fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content { return Content.Builder() .dataMessage( DataMessage.Builder().buildWith { + timestamp = sentTimestamp body = string() if (random.nextBoolean()) { expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt()) @@ -87,6 +89,20 @@ object MessageContentFuzzer { .build() } + /** + * Create an edit message. + */ + fun editTextMessage(targetTimestamp: Long, editedDataMessage: DataMessage): Content { + return Content.Builder() + .editMessage( + EditMessage.Builder().buildWith { + targetSentTimestamp = targetTimestamp + dataMessage = editedDataMessage + } + ) + .build() + } + /** * Create a sync sent text message for the given [DataMessage]. */ @@ -116,6 +132,24 @@ object MessageContentFuzzer { ).build() } + /** + * Create a sync reads message for the given [RecipientId] and message timestamp pairings. + */ + fun syncReadsMessage(timestamps: List>): Content { + return Content + .Builder() + .syncMessage( + SyncMessage.Builder().buildWith { + read = timestamps.map { (senderId, timestamp) -> + SyncMessage.Read.Builder().buildWith { + this.senderAci = Recipient.resolved(senderId).requireAci().toString() + this.timestamp = timestamp + } + } + } + ).build() + } + /** * Create a random media message that may be: * - A text body @@ -184,22 +218,21 @@ object MessageContentFuzzer { } /** - * Create a random media message that can never contain a text body. It may be: - * - A sticker + * Create a random media message that contains a sticker. */ - fun fuzzMediaMessageNoText(previousMessages: List = emptyList()): Content { + fun fuzzStickerMediaMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content { return Content.Builder() .dataMessage( DataMessage.Builder().buildWith { - if (random.nextFloat() < 0.9) { - sticker = DataMessage.Sticker.Builder().buildWith { - packId = byteString(length = 24) - packKey = byteString(length = 128) - stickerId = random.nextInt() - data_ = attachmentPointer() - emoji = emojis.random(random) - } + timestamp = sentTimestamp + sticker = DataMessage.Sticker.Builder().buildWith { + packId = byteString(length = 24) + packKey = byteString(length = 128) + stickerId = random.nextInt() + data_ = attachmentPointer() + emoji = emojis.random(random) } + groupV2 = groupContextV2 } ).build() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 0fd2e5f480..c90c9c671f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -136,7 +136,6 @@ import org.thoughtcrime.securesms.util.MessageConstraintsUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.isStory -import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.internal.push.SyncMessage import java.io.Closeable @@ -4379,17 +4378,17 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat /** * @return Unhandled ids */ - fun setTimestampReadFromSyncMessage(readMessages: List, proposedExpireStarted: Long, threadToLatestRead: MutableMap): Collection { + fun setTimestampReadFromSyncMessage(readMessages: List, proposedExpireStarted: Long, threadToLatestRead: MutableMap): Collection { val expiringMessages: MutableList> = mutableListOf() val updatedThreads: MutableSet = mutableSetOf() val unhandled: MutableCollection = mutableListOf() writableDatabase.withinTransaction { for (readMessage in readMessages) { - val authorId: RecipientId = recipients.getOrInsertFromServiceId(readMessage.sender) + val authorId: RecipientId = recipients.getOrInsertFromServiceId(ServiceId.parseOrThrow(readMessage.senderAci!!)) val result: TimestampReadResult = setTimestampReadFromSyncMessageInternal( - messageId = SyncMessageId(authorId, readMessage.timestamp), + messageId = SyncMessageId(authorId, readMessage.timestamp!!), proposedExpireStarted = proposedExpireStarted, threadToLatestRead = threadToLatestRead ) @@ -4398,7 +4397,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat updatedThreads += result.threads if (result.threads.isEmpty()) { - unhandled += SyncMessageId(authorId, readMessage.timestamp) + unhandled += SyncMessageId(authorId, readMessage.timestamp!!) } } @@ -4419,12 +4418,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat return unhandled } - fun setTimestampReadFromSyncMessageProto(readMessages: List, proposedExpireStarted: Long, threadToLatestRead: MutableMap): Collection { - val reads: List = readMessages.map { r -> ReadMessage(ServiceId.parseOrThrow(r.senderAci!!), r.timestamp!!) } - - return setTimestampReadFromSyncMessage(reads, proposedExpireStarted, threadToLatestRead) - } - /** * Handles a synchronized read message. * @param messageId An id representing the author-timestamp pair of the message that was read on a linked device. Note that the author could be self when diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index 9fdaced7c1..3e37771980 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.CallLinkTable import org.thoughtcrime.securesms.database.CallTable import org.thoughtcrime.securesms.database.GroupReceiptTable import org.thoughtcrime.securesms.database.GroupTable +import org.thoughtcrime.securesms.database.MessageTable import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo import org.thoughtcrime.securesms.database.NoSuchMessageException import org.thoughtcrime.securesms.database.PaymentMetaDataUtil @@ -916,9 +917,9 @@ object SyncMessageProcessor { ) { log(envelopeTimestamp, "Synchronize read message. Count: ${readMessages.size}, Timestamps: ${readMessages.map { it.timestamp }}") - val threadToLatestRead: Map = HashMap() - val unhandled = SignalDatabase.messages.setTimestampReadFromSyncMessageProto(readMessages, envelopeTimestamp, threadToLatestRead.toMutableMap()) - val markedMessages: List = SignalDatabase.threads.setReadSince(threadToLatestRead, false) + val threadToLatestRead: MutableMap = HashMap() + val unhandled: Collection = SignalDatabase.messages.setTimestampReadFromSyncMessage(readMessages, envelopeTimestamp, threadToLatestRead) + val markedMessages: List = SignalDatabase.threads.setReadSince(threadToLatestRead, false) if (Util.hasItems(markedMessages)) { log("Updating past SignalDatabase.messages: " + markedMessages.size)