diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTest.kt new file mode 100644 index 0000000000..e9bd146db8 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessorTest.kt @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.messages + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import org.junit.Rule +import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata +import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState +import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.whispersystems.signalservice.api.messages.SignalServiceContent + +abstract class MessageContentProcessorTest { + + @get:Rule + val harness = SignalActivityRule() + + protected fun MessageContentProcessor.doProcess( + messageState: MessageState = MessageState.DECRYPTED_OK, + content: SignalServiceContent, + exceptionMetadata: ExceptionMetadata = ExceptionMetadata("sender", 1), + timestamp: Long = 100L, + smsMessageId: Long = -1L + ) { + process(messageState, content, exceptionMetadata, timestamp, smsMessageId) + } + + protected fun createNormalContentTestSubject(): MessageContentProcessor { + val context = ApplicationProvider.getApplicationContext() + + return MessageContentProcessor.forNormalContent(context) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt new file mode 100644 index 0000000000..31cedcf7a4 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleStoryMessageTest.kt @@ -0,0 +1,124 @@ +package org.thoughtcrime.securesms.messages + +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.signal.core.util.requireLong +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.MessageDatabase +import org.thoughtcrime.securesms.database.MmsHelper +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord +import org.thoughtcrime.securesms.database.model.ParentStoryId +import org.thoughtcrime.securesms.database.model.StoryType +import org.thoughtcrime.securesms.mms.IncomingMediaMessage +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.testing.TestProtos +import org.thoughtcrime.securesms.util.FeatureFlagsTestUtil +import org.whispersystems.signalservice.api.messages.SignalServiceContent +import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto +import kotlin.random.Random + +@Suppress("ClassName") +class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorTest() { + + @Before + fun setUp() { + FeatureFlagsTestUtil.setStoriesEnabled(true) + SignalDatabase.mms.deleteAllThreads() + } + + @After + fun tearDown() { + SignalDatabase.mms.deleteAllThreads() + } + + @Test + fun givenContentWithAGroupStoryReplyWhenIProcessThenIInsertAReplyToTheCorrectStory() { + val sender = Recipient.resolved(harness.others[0]) + val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE)) + val decryptedGroupState = DecryptedGroup.newBuilder() + .addAllMembers( + listOf( + DecryptedMember.newBuilder() + .setUuid(harness.self.requireServiceId().toByteString()) + .setJoinedAtRevision(0) + .setRole(Member.Role.DEFAULT) + .build(), + DecryptedMember.newBuilder() + .setUuid(sender.requireServiceId().toByteString()) + .setJoinedAtRevision(0) + .setRole(Member.Role.DEFAULT) + .build() + ) + ) + .setRevision(0) + .build() + + val group = SignalDatabase.groups.create( + groupMasterKey, + decryptedGroupState + ) + + val groupRecipient = Recipient.externalGroupExact(group) + val threadForGroup = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient) + + val insertResult = MmsHelper.insert( + message = IncomingMediaMessage( + from = sender.id, + sentTimeMillis = 100L, + serverTimeMillis = 101L, + receivedTimeMillis = 102L, + storyType = StoryType.STORY_WITH_REPLIES + ), + threadId = threadForGroup + ) + + val expectedBody = "Hello, World!" + val storyContent: SignalServiceContentProto = TestProtos.build { + serviceContent( + localAddress = address(uuid = harness.self.requireServiceId().uuid()).build(), + metadata = metadata( + address = address(uuid = sender.requireServiceId().uuid()).build() + ).build() + ).apply { + content = content().apply { + dataMessage = dataMessage().apply { + storyContext = storyContext( + sentTimestamp = 100L, + authorUuid = sender.requireServiceId().toString() + ).build() + + groupV2 = groupContextV2(masterKeyBytes = groupMasterKey.serialize()).build() + body = expectedBody + }.build() + }.build() + }.build() + } + + runTestWithContent(storyContent) + + val replyId = SignalDatabase.mms.getStoryReplies(insertResult.get().messageId).use { cursor -> + assertEquals(1, cursor.count) + cursor.moveToFirst() + cursor.requireLong(MessageDatabase.ID) + } + + val replyRecord = SignalDatabase.mms.getMessageRecord(replyId) as MediaMmsMessageRecord + assertEquals(ParentStoryId.GroupReply(insertResult.get().messageId).serialize(), replyRecord.parentStoryId?.serialize()) + assertEquals(threadForGroup, replyRecord.threadId) + assertEquals(expectedBody, replyRecord.body) + + SignalDatabase.mms.deleteGroupStoryReplies(insertResult.get().messageId) + } + + private fun runTestWithContent(contentProto: SignalServiceContentProto) { + val content = SignalServiceContent.createFromProto(contentProto) + val testSubject = createNormalContentTestSubject() + testSubject.doProcess(content = content) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt new file mode 100644 index 0000000000..3d0e9c5ea1 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/MessageContentProcessor__handleTextMessageTest.kt @@ -0,0 +1,36 @@ +package org.thoughtcrime.securesms.messages + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.testing.TestProtos +import org.whispersystems.signalservice.api.messages.SignalServiceContent +import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto + +@Suppress("ClassName") +class MessageContentProcessor__handleTextMessageTest : MessageContentProcessorTest() { + @Test + fun givenContentWithATextMessageWhenIProcessThenIInsertTheTextMessage() { + val testSubject: MessageContentProcessor = createNormalContentTestSubject() + val expectedBody = "Hello, World!" + val contentProto: SignalServiceContentProto = TestProtos.build { + val dataMessage = dataMessage().apply { body = expectedBody } + val content = content().apply { this.dataMessage = dataMessage.build() } + serviceContent().apply { this.content = content.build() } + }.build() + + val content = SignalServiceContent.createFromProto(contentProto) + + // WHEN + testSubject.doProcess(content = content) + + // THEN + val record = SignalDatabase.sms.getMessageRecord(1) + val threadSize = SignalDatabase.mmsSms.getConversationCount(record.threadId) + assertEquals(1, threadSize) + + assertTrue(record.isSecure) + assertEquals(expectedBody, record.body) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/TestProtos.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/TestProtos.kt new file mode 100644 index 0000000000..5d2efabcd5 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/TestProtos.kt @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.testing + +import com.google.protobuf.ByteString +import org.signal.libsignal.zkgroup.groups.GroupMasterKey +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.internal.push.SignalServiceProtos +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2 +import org.whispersystems.signalservice.internal.serialize.protos.AddressProto +import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto +import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto +import java.util.UUID +import kotlin.random.Random + +class TestProtos private constructor() { + fun address( + uuid: UUID = UUID.randomUUID() + ): AddressProto.Builder { + return AddressProto.newBuilder() + .setUuid(ServiceId.from(uuid).toByteString()) + } + + fun metadata( + address: AddressProto = address().build(), + ): MetadataProto.Builder { + return MetadataProto.newBuilder() + .setAddress(address) + } + + fun groupContextV2( + revision: Int = 0, + masterKeyBytes: ByteArray = Random.Default.nextBytes(GroupMasterKey.SIZE) + ): GroupContextV2.Builder { + return GroupContextV2.newBuilder() + .setRevision(revision) + .setMasterKey(ByteString.copyFrom(masterKeyBytes)) + } + + fun storyContext( + sentTimestamp: Long = Random.nextLong(), + authorUuid: String = UUID.randomUUID().toString() + ): DataMessage.StoryContext.Builder { + return DataMessage.StoryContext.newBuilder() + .setAuthorUuid(authorUuid) + .setSentTimestamp(sentTimestamp) + } + + fun dataMessage(): DataMessage.Builder { + return DataMessage.newBuilder() + } + + fun content(): SignalServiceProtos.Content.Builder { + return SignalServiceProtos.Content.newBuilder() + } + + fun serviceContent( + localAddress: AddressProto = address().build(), + metadata: MetadataProto = metadata().build() + ): SignalServiceContentProto.Builder { + return SignalServiceContentProto.newBuilder() + .setLocalAddress(localAddress) + .setMetadata(metadata) + } + + companion object { + fun build(buildFn: TestProtos.() -> T): T { + return TestProtos().buildFn() + } + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/util/FeatureFlagsTestUtil.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/util/FeatureFlagsTestUtil.kt new file mode 100644 index 0000000000..c8348c45ed --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/util/FeatureFlagsTestUtil.kt @@ -0,0 +1,10 @@ +package org.thoughtcrime.securesms.util + +/** + * Utility to enable / disable feature flags via forced values. + */ +object FeatureFlagsTestUtil { + fun setStoriesEnabled(isEnabled: Boolean) { + FeatureFlags.FORCED_VALUES[FeatureFlags.STORIES] = isEnabled + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 7c8a7764ac..0506712420 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -82,7 +82,7 @@ public final class FeatureFlags { private static final String RETRY_RECEIPTS = "android.retryReceipts"; private static final String MAX_GROUP_CALL_RING_SIZE = "global.calling.maxGroupCallRingSize"; private static final String GROUP_CALL_RINGING = "android.calling.groupCallRinging"; - private static final String STORIES = "android.stories.7"; + static final String STORIES = "android.stories.7"; private static final String STORIES_TEXT_FUNCTIONS = "android.stories.text.functions"; private static final String HARDWARE_AEC_BLOCKLIST_MODELS = "android.calling.hardwareAecBlockList"; private static final String SOFTWARE_AEC_BLOCKLIST_MODELS = "android.calling.softwareAecBlockList";