Release polls behind feature flag.

This commit is contained in:
Michelle Tang
2025-10-01 12:46:37 -04:00
parent 67a693107e
commit b8e4ffb5ae
84 changed files with 4164 additions and 102 deletions

View File

@@ -36,6 +36,8 @@ import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.polls.PollOption
import org.thoughtcrime.securesms.polls.PollRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
@@ -348,5 +350,11 @@ class V2ConversationItemShapeTest {
override fun onShowUnverifiedProfileSheet(forGroup: Boolean) = Unit
override fun onUpdateSignalClicked() = Unit
override fun onViewResultsClicked(pollId: Long) = Unit
override fun onViewPollClicked(messageId: Long) = Unit
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) = Unit
}
}

View File

@@ -0,0 +1,131 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.polls.PollOption
import org.thoughtcrime.securesms.polls.PollRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalActivityRule
@RunWith(AndroidJUnit4::class)
class PollTablesTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var poll1: PollRecord
@Before
fun setUp() {
poll1 = PollRecord(
id = 1,
question = "how do you feel about unit testing?",
pollOptions = listOf(
PollOption(1, "yay", listOf(1)),
PollOption(2, "ok", emptyList()),
PollOption(3, "nay", emptyList())
),
allowMultipleVotes = false,
hasEnded = false,
authorId = 1,
messageId = 1
)
SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollTable.TABLE_NAME)
SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollOptionTable.TABLE_NAME)
SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollVoteTable.TABLE_NAME)
val message = IncomingMessage(type = MessageType.NORMAL, from = harness.others[0], sentTimeMillis = 100, serverTimeMillis = 100, receivedTimeMillis = 100)
SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(harness.others[0], isGroup = false))
}
@Test
fun givenAPollWithVoting_whenIGetPoll_thenIExpectThatPoll() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 1, messageId = MessageId(1))
assertEquals(poll1, SignalDatabase.polls.getPoll(1))
}
@Test
fun givenAPoll_whenIGetItsOptionIds_thenIExpectAllOptionsIds() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
assertEquals(poll1.pollOptions.map { it.id }, SignalDatabase.polls.getPollOptionIds(1))
}
@Test
fun givenAPollAndVoter_whenIGetItsVoteCount_thenIExpectTheCorrectVoterCount() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 1, messageId = MessageId(1))
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(2), voterId = 2, voteCount = 2, messageId = MessageId(1))
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(3), voterId = 3, voteCount = 3, messageId = MessageId(1))
assertEquals(1, SignalDatabase.polls.getCurrentPollVoteCount(1, 1))
assertEquals(2, SignalDatabase.polls.getCurrentPollVoteCount(1, 2))
assertEquals(3, SignalDatabase.polls.getCurrentPollVoteCount(1, 3))
}
@Test
fun givenMultipleRoundsOfVoting_whenIGetItsCount_thenIExpectTheMostRecentResults() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(2), voterId = 1, voteCount = 1, messageId = MessageId(1))
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(3), voterId = 1, voteCount = 2, messageId = MessageId(1))
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 3, messageId = MessageId(1))
assertEquals(poll1, SignalDatabase.polls.getPoll(1))
}
@Test
fun givenAPoll_whenITerminateIt_thenIExpectItToEnd() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
SignalDatabase.polls.endPoll(1, System.currentTimeMillis())
assertEquals(true, SignalDatabase.polls.getPoll(1)!!.hasEnded)
}
@Test
fun givenAPoll_whenIIVote_thenIExpectThatVote() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
val poll = SignalDatabase.polls.getPoll(1)!!
val pollOption = poll.pollOptions.first()
val voteCount = SignalDatabase.polls.insertVote(poll, pollOption)
assertEquals(1, voteCount)
assertEquals(listOf(0), SignalDatabase.polls.getVotes(poll.id, false))
}
@Test
fun givenAPoll_whenIRemoveVote_thenVoteIsCleared() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
val poll = SignalDatabase.polls.getPoll(1)!!
val pollOption = poll.pollOptions.first()
val voteCount = SignalDatabase.polls.removeVote(poll, pollOption)
SignalDatabase.polls.markPendingAsRemoved(poll.id, Recipient.self().id.toLong(), voteCount, 1)
assertEquals(1, voteCount)
val status = SignalDatabase.polls.getPollVoteStateForGivenVote(poll.id, voteCount)
assertEquals(PollTables.VoteState.REMOVED, status)
}
@Test
fun givenAVote_whenISetPollOptionId_thenOptionIdIsUpdated() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
val poll = SignalDatabase.polls.getPoll(1)!!
val option = poll.pollOptions.first()
SignalDatabase.polls.insertVotes(poll.id, listOf(option.id), Recipient.self().id.toLong(), 5, MessageId(1))
SignalDatabase.polls.setPollVoteStateForGivenVote(poll.id, Recipient.self().id.toLong(), 5, 1, true)
val status = SignalDatabase.polls.getPollVoteStateForGivenVote(poll.id, 5)
assertEquals(PollTables.VoteState.ADDED, status)
}
}

View File

@@ -0,0 +1,322 @@
package org.thoughtcrime.securesms.messages
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageType
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
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.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.DataMessage
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class DataMessageProcessorTest_polls {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var alice: Recipient
private lateinit var bob: Recipient
private lateinit var charlie: Recipient
private lateinit var groupId: GroupId.V2
private lateinit var groupRecipientId: RecipientId
@Before
fun setUp() {
alice = Recipient.resolved(harness.others[0])
bob = Recipient.resolved(harness.others[1])
charlie = Recipient.resolved(harness.others[2])
val groupInfo = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), alice.asMember(), bob.asMember())
groupId = groupInfo.groupId
groupRecipientId = groupInfo.recipientId
}
@Test
fun handlePollCreate_whenIHaveAValidPollProto_createPoll() {
val insertResult = handlePollCreate(
pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false),
senderRecipient = alice,
threadRecipient = Recipient.resolved(groupRecipientId),
groupId = groupId
)
assert(insertResult != null)
val poll = SignalDatabase.polls.getPoll(insertResult!!.messageId)
assert(poll != null)
assertThat(poll!!.question).isEqualTo("question?")
assertThat(poll.pollOptions.size).isEqualTo(3)
assertThat(poll.allowMultipleVotes).isEqualTo(false)
assertThat(poll.hasEnded).isEqualTo(false)
}
@Test
fun handlePollCreate_whenSenderIsNotInGroup_dropMessage() {
val insertResult = handlePollCreate(
pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false),
senderRecipient = charlie,
threadRecipient = Recipient.resolved(groupRecipientId),
groupId = groupId
)
assert(insertResult == null)
}
@Test
fun handlePollCreate_whenTargetRecipientIsNotAGroup_dropMessage() {
val insertResult = handlePollCreate(
pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false),
senderRecipient = alice,
threadRecipient = bob,
groupId = null
)
assert(insertResult == null)
}
@Test
fun handlePollTerminate_whenIHaveValidProto_endPoll() {
val pollMessageId = insertPoll()
val insertResult = DataMessageProcessor.handlePollTerminate(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(200),
message = DataMessage(pollTerminate = DataMessage.PollTerminate(targetSentTimestamp = 100)),
senderRecipient = alice,
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
threadRecipient = bob,
groupId = groupId,
receivedTime = 200
)
assert(insertResult?.messageId != null)
val poll = SignalDatabase.polls.getPoll(pollMessageId)
assert(poll != null)
assert(poll!!.hasEnded)
}
@Test
fun handlePollTerminate_whenIHaveDifferentTimestamp_dropMessage() {
insertPoll()
val insertResult = DataMessageProcessor.handlePollTerminate(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(200),
message = DataMessage(pollTerminate = DataMessage.PollTerminate(200)),
senderRecipient = alice,
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
threadRecipient = bob,
groupId = groupId,
receivedTime = 200
)
assert(insertResult == null)
}
@Test
fun handlePollTerminate_whenMessageIsNotFromCreatorOfPoll_dropMessage() {
insertPoll()
val insertResult = DataMessageProcessor.handlePollTerminate(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(200),
message = DataMessage(pollTerminate = DataMessage.PollTerminate(100)),
senderRecipient = bob,
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
threadRecipient = bob,
groupId = groupId,
receivedTime = 200
)
assert(insertResult == null)
}
@Test
fun handlePollTerminate_whenPollDoesNotExist_dropMessage() {
val insertResult = DataMessageProcessor.handlePollTerminate(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(200),
message = DataMessage(pollTerminate = DataMessage.PollTerminate(100)),
senderRecipient = alice,
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
threadRecipient = bob,
groupId = groupId,
receivedTime = 200
)
assert(insertResult == null)
}
@Test
fun handlePollVote_whenValidPollVote_processVote() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0),
voteCount = 1
),
bob
)
assert(messageId != null)
assertThat(messageId!!.id).isEqualTo(1)
val poll = SignalDatabase.polls.getPoll(messageId.id)
assert(poll != null)
assertThat(poll!!.pollOptions[0].voterIds).isEqualTo(listOf(bob.id.toLong()))
}
@Test
fun handlePollVote_whenMultipleVoteAllowed_processAllVote() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = 1
),
bob
)
assert(messageId != null)
val poll = SignalDatabase.polls.getPoll(messageId!!.id)
assert(poll != null)
assertThat(poll!!.pollOptions[0].voterIds).isEqualTo(listOf(bob.id.toLong()))
assertThat(poll.pollOptions[1].voterIds).isEqualTo(listOf(bob.id.toLong()))
assertThat(poll.pollOptions[2].voterIds).isEqualTo(listOf(bob.id.toLong()))
}
@Test
fun handlePollVote_whenMultipleVoteSentToSingleVotePolls_dropMessage() {
insertPoll(false)
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = 1
),
bob
)
assert(messageId == null)
}
@Test
fun handlePollVote_whenVoteCountIsNotHigher_dropMessage() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = -1
),
bob
)
assert(messageId == null)
}
@Test
fun handlePollVote_whenVoteOptionDoesNotExist_dropMessage() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(5),
voteCount = 1
),
bob
)
assert(messageId == null)
}
@Test
fun handlePollVote_whenVoterNotInGroup_dropMessage() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = 1
),
charlie
)
assert(messageId == null)
}
@Test
fun handlePollVote_whenPollDoesNotExist_dropMessage() {
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = 1
),
bob
)
assert(messageId == null)
}
private fun handlePollCreate(pollCreate: DataMessage.PollCreate, senderRecipient: Recipient, threadRecipient: Recipient, groupId: GroupId.V2?): MessageTable.InsertResult? {
return DataMessageProcessor.handlePollCreate(
envelope = MessageContentFuzzer.envelope(100),
message = DataMessage(pollCreate = pollCreate),
senderRecipient = senderRecipient,
threadRecipient = threadRecipient,
groupId = groupId,
receivedTime = 0,
context = ApplicationProvider.getApplicationContext(),
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId())
)
}
private fun handlePollVote(pollVote: DataMessage.PollVote, senderRecipient: Recipient): MessageId? {
return DataMessageProcessor.handlePollVote(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(100),
message = DataMessage(pollVote = pollVote),
senderRecipient = senderRecipient,
earlyMessageCacheEntry = null
)
}
private fun insertPoll(allowMultiple: Boolean = true): Long {
val envelope = MessageContentFuzzer.envelope(100)
val pollMessage = IncomingMessage(type = MessageType.NORMAL, from = alice.id, sentTimeMillis = envelope.timestamp!!, serverTimeMillis = envelope.serverTimestamp!!, receivedTimeMillis = 0, groupId = groupId)
val messageId = SignalDatabase.messages.insertMessageInbox(pollMessage).get()
SignalDatabase.polls.insertPoll("question?", allowMultiple, listOf("a", "b", "c"), alice.id.toLong(), messageId.messageId)
return messageId.messageId
}
}