package org.thoughtcrime.securesms.database import androidx.test.ext.junit.runners.AndroidJUnit4 import junit.framework.TestCase.assertNull import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.hasSize import org.hamcrest.Matchers.`is` import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.StoryType import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.whispersystems.signalservice.api.push.DistributionId import org.whispersystems.signalservice.api.push.ServiceId import java.util.UUID @RunWith(AndroidJUnit4::class) class StorySendTableTest { private val distributionId1 = DistributionId.from(UUID.randomUUID()) private val distributionId2 = DistributionId.from(UUID.randomUUID()) private val distributionId3 = DistributionId.from(UUID.randomUUID()) private lateinit var distributionList1: DistributionListId private lateinit var distributionList2: DistributionListId private lateinit var distributionList3: DistributionListId private lateinit var distributionListRecipient1: Recipient private lateinit var distributionListRecipient2: Recipient private lateinit var distributionListRecipient3: Recipient private lateinit var recipients1to10: List private lateinit var recipients11to20: List private lateinit var recipients6to15: List private lateinit var recipients6to10: List private var messageId1: Long = 0 private var messageId2: Long = 0 private var messageId3: Long = 0 private lateinit var storySends: StorySendTable @Before fun setup() { storySends = SignalDatabase.storySends recipients1to10 = makeRecipients(10) recipients11to20 = makeRecipients(10) distributionList1 = SignalDatabase.distributionLists.createList("1", emptyList(), distributionId = distributionId1)!! distributionList2 = SignalDatabase.distributionLists.createList("2", emptyList(), distributionId = distributionId2)!! distributionList3 = SignalDatabase.distributionLists.createList("3", emptyList(), distributionId = distributionId3)!! distributionListRecipient1 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList1)) distributionListRecipient2 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList2)) distributionListRecipient3 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList3)) messageId1 = MmsHelper.insert( recipient = distributionListRecipient1, storyType = StoryType.STORY_WITHOUT_REPLIES, ) messageId2 = MmsHelper.insert( recipient = distributionListRecipient2, storyType = StoryType.STORY_WITH_REPLIES, ) messageId3 = MmsHelper.insert( recipient = distributionListRecipient3, storyType = StoryType.STORY_WITHOUT_REPLIES, ) recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5) recipients6to10 = recipients1to10.takeLast(5) } @Test fun getRecipientsToSendTo_noOverlap() { storySends.insert(messageId1, recipients1to10, 100, false, distributionId1) storySends.insert(messageId2, recipients11to20, 200, true, distributionId2) storySends.insert(messageId3, recipients1to10, 300, false, distributionId3) val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false) val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true) assertThat(recipientIdsForMessage1, hasSize(10)) assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray())) assertThat(recipientIdsForMessage2, hasSize(10)) assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray())) } @Test fun getRecipientsToSendTo_overlap() { storySends.insert(messageId1, recipients1to10, 100, false, distributionId1) storySends.insert(messageId2, recipients6to15, 100, true, distributionId2) val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false) val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true) assertThat(recipientIdsForMessage1, hasSize(5)) assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray())) assertThat(recipientIdsForMessage2, hasSize(10)) assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray())) } @Test fun getRecipientsToSendTo_overlapAll() { val recipient1 = recipients1to10.first() val recipient2 = recipients11to20.first() storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false, distributionId1) storySends.insert(messageId2, listOf(recipient1), 100, true, distributionId2) storySends.insert(messageId3, listOf(recipient2), 100, true, distributionId3) val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false) val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true) val recipientIdsForMessage3 = storySends.getRecipientsToSendTo(messageId3, 100, true) assertThat(recipientIdsForMessage1, hasSize(0)) assertThat(recipientIdsForMessage2, hasSize(1)) assertThat(recipientIdsForMessage2, containsInAnyOrder(recipient1)) assertThat(recipientIdsForMessage3, hasSize(1)) assertThat(recipientIdsForMessage3, containsInAnyOrder(recipient2)) } @Test fun getRecipientsToSendTo_overlapWithEarlierMessage() { storySends.insert(messageId1, recipients6to15, 100, true, distributionId1) storySends.insert(messageId2, recipients1to10, 100, false, distributionId2) val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true) val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false) assertThat(recipientIdsForMessage1, hasSize(10)) assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients6to15.toTypedArray())) assertThat(recipientIdsForMessage2, hasSize(5)) assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients1to10.take(5).toTypedArray())) } @Test fun getRemoteDeleteRecipients_noOverlap() { storySends.insert(messageId1, recipients1to10, 100, false, distributionId1) storySends.insert(messageId2, recipients11to20, 200, true, distributionId2) storySends.insert(messageId3, recipients1to10, 300, false, distributionId3) val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100) val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200) assertThat(recipientIdsForMessage1, hasSize(10)) assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray())) assertThat(recipientIdsForMessage2, hasSize(10)) assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray())) } @Test fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId2, recipients6to15, 200, true, distributionId2) val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200) val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200) assertThat(recipientIdsForMessage1, hasSize(5)) assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray())) assertThat(recipientIdsForMessage2, hasSize(5)) assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.takeLast(5).toTypedArray())) } @Test fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) SignalDatabase.mms.markAsRemoteDelete(messageId1) storySends.insert(messageId2, recipients6to15, 200, true, distributionId2) val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200) assertThat(recipientIdsForMessage2, hasSize(10)) assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray())) } @Test fun canReply_storyWithReplies() { storySends.insert(messageId2, recipients1to10, 200, true, distributionId2) val canReply = storySends.canReply(recipients1to10[0], 200) assertThat(canReply, `is`(true)) } @Test fun canReply_storyWithoutReplies() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) val canReply = storySends.canReply(recipients1to10[0], 200) assertThat(canReply, `is`(false)) } @Test fun canReply_storyWithAndWithoutRepliesOverlap() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId2, recipients6to10, 200, true, distributionId2) val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200) val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200) assertThat(message1OnlyRecipientCanReply, `is`(false)) assertThat(message2RecipientCanReply, `is`(true)) } @Test fun givenASingleStory_whenIGetFullSentStorySyncManifest_thenIExpectNotNull() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200) assertNotNull(manifest) } @Test fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNull() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId2, recipients1to10, 200, false, distributionId2) val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200) assertNull(manifest) } @Test fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectOneManifestPerRecipient() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId2, recipients1to10, 200, true, distributionId2) val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!! assertEquals(recipients1to10, manifest.entries.map { it.recipientId }) } @Test fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectTwoListsPerRecipient() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId2, recipients1to10, 200, true, distributionId2) val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!! manifest.entries.forEach { entry -> assertEquals(listOf(distributionId1, distributionId2), entry.distributionLists) } } @Test fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectAllRecipientsCanReply() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId2, recipients1to10, 200, true, distributionId2) val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!! manifest.entries.forEach { entry -> assertTrue(entry.allowedToReply) } } @Test fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNonNullResult() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId2, recipients1to10, 200, true, distributionId2) SignalDatabase.mms.markAsRemoteDelete(messageId1) val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)!! assertNotNull(manifest) } /* @Test fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetRecipientIdsForManifestUpdate_thenIExpectOnlyRecipientsWithStory2() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId1, recipients11to20, 200, false, distributionId1) storySends.insert(messageId2, recipients1to10, 200, true, distributionId2) SignalDatabase.mms.markAsRemoteDelete(messageId1) val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1) assertEquals(recipients1to10.toHashSet(), recipientIds) } @Test fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectOnlyRecipientsThatHadStory1() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId2, recipients1to10, 200, true, distributionId2) storySends.insert(messageId2, recipients11to20, 200, true, distributionId2) SignalDatabase.mms.markAsRemoteDelete(messageId1) val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1) val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds) val manifestRecipients = results.entries.map { it.recipientId } assertEquals(recipients1to10, manifestRecipients) } @Test fun givenTwoStoriesAndTheOneThatAllowedRepliesIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectAllowRepliesToBeTrue() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) storySends.insert(messageId2, recipients1to10, 200, true, distributionId2) SignalDatabase.mms.markAsRemoteDelete(messageId2) val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1) val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds) assertTrue(results.entries.all { it.allowedToReply }) } */ @Test fun givenEmptyManifest_whenIApplyRemoteManifest_thenNothingChanges() { storySends.insert(messageId1, recipients1to10, 200, false, distributionId1) val expected = storySends.getFullSentStorySyncManifest(messageId1, 200) val emptyManifest = SentStorySyncManifest(emptyList()) storySends.applySentStoryManifest(emptyManifest, 200) val result = storySends.getFullSentStorySyncManifest(messageId1, 200) assertEquals(expected, result) } @Test fun givenAnIdenticalManifest_whenIApplyRemoteManifest_thenNothingChanges() { val messageId4 = MmsHelper.insert( recipient = distributionListRecipient1, storyType = StoryType.STORY_WITHOUT_REPLIES, sentTimeMillis = 200 ) storySends.insert(messageId4, recipients1to10, 200, false, distributionId1) val expected = storySends.getFullSentStorySyncManifest(messageId4, 200) storySends.applySentStoryManifest(expected!!, 200) val result = storySends.getFullSentStorySyncManifest(messageId4, 200) assertEquals(expected, result) } @Test fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeMarkedRemoteDeleted() { val messageId4 = MmsHelper.insert( recipient = distributionListRecipient1, storyType = StoryType.STORY_WITHOUT_REPLIES, sentTimeMillis = 200 ) val messageId5 = MmsHelper.insert( recipient = distributionListRecipient2, storyType = StoryType.STORY_WITHOUT_REPLIES, sentTimeMillis = 200 ) storySends.insert(messageId4, recipients1to10, 200, false, distributionId1) val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!! storySends.insert(messageId5, recipients1to10, 200, false, distributionId2) storySends.applySentStoryManifest(remote, 200) assertTrue(SignalDatabase.mms.getMessageRecord(messageId5).isRemoteDelete) } @Test fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectSharedMessageToNotBeMarkedRemoteDeleted() { val messageId4 = MmsHelper.insert( recipient = distributionListRecipient1, storyType = StoryType.STORY_WITHOUT_REPLIES, sentTimeMillis = 200 ) val messageId5 = MmsHelper.insert( recipient = distributionListRecipient2, storyType = StoryType.STORY_WITHOUT_REPLIES, sentTimeMillis = 200 ) storySends.insert(messageId4, recipients1to10, 200, false, distributionId1) val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!! storySends.insert(messageId5, recipients1to10, 200, false, distributionId2) storySends.applySentStoryManifest(remote, 200) assertFalse(SignalDatabase.mms.getMessageRecord(messageId4).isRemoteDelete) } @Test fun givenNoLocalEntries_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatch() { val messageId4 = MmsHelper.insert( recipient = distributionListRecipient1, storyType = StoryType.STORY_WITHOUT_REPLIES, sentTimeMillis = 2000 ) val remote = SentStorySyncManifest( recipients1to10.map { SentStorySyncManifest.Entry( recipientId = it, allowedToReply = true, distributionLists = listOf(distributionId1) ) } ) storySends.applySentStoryManifest(remote, 2000) val local = storySends.getFullSentStorySyncManifest(messageId4, 2000) assertEquals(remote, local) } @Test fun givenNonStoryMessageAtSentTimestamp_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatchAndNoCrashes() { val messageId4 = MmsHelper.insert( recipient = distributionListRecipient1, storyType = StoryType.STORY_WITHOUT_REPLIES, sentTimeMillis = 2000 ) MmsHelper.insert( recipient = Recipient.resolved(recipients1to10.first()), sentTimeMillis = 2000 ) val remote = SentStorySyncManifest( recipients1to10.map { SentStorySyncManifest.Entry( recipientId = it, allowedToReply = true, distributionLists = listOf(distributionId1) ) } ) storySends.applySentStoryManifest(remote, 2000) val local = storySends.getFullSentStorySyncManifest(messageId4, 2000) assertEquals(remote, local) } private fun makeRecipients(count: Int): List { return (1..count).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) } } }