mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-24 11:45:28 +00:00
Release polls behind feature flag.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user