Merge pull request #7851 from vector-im/feature/mna/poll-message-decryption-error

[Poll] Warning message on decryption failure of some events (PSG-1025)
This commit is contained in:
Maxime NATUREL 2023-01-16 10:19:00 +01:00 committed by GitHub
commit 6b98b3023e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 746 additions and 75 deletions

1
changelog.d/7824.feature Normal file
View File

@ -0,0 +1 @@
[Poll] Warning message on decryption failure of some events

View File

@ -3196,6 +3196,7 @@
<string name="closed_poll_option_title">Closed poll</string> <string name="closed_poll_option_title">Closed poll</string>
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string> <string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
<string name="ended_poll_indicator">Ended the poll.</string> <string name="ended_poll_indicator">Ended the poll.</string>
<string name="unable_to_decrypt_some_events_in_poll">Due to decryption errors, some votes may not be counted</string>
<string name="room_polls_active">Active polls</string> <string name="room_polls_active">Active polls</string>
<string name="room_polls_active_no_item">There are no active polls in this room</string> <string name="room_polls_active_no_item">There are no active polls in this room</string>
<string name="room_polls_ended">Past polls</string> <string name="room_polls_ended">Past polls</string>

View File

@ -23,5 +23,7 @@ data class PollResponseAggregatedSummary(
val nbOptions: Int = 0, val nbOptions: Int = 0,
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
val sourceEvents: List<String>, val sourceEvents: List<String>,
val localEchos: List<String> val localEchos: List<String>,
// list of related event ids which are encrypted due to decryption failure
val encryptedRelatedEventIds: List<String>,
) )

View File

@ -59,7 +59,7 @@ internal class EventDecryptor @Inject constructor(
private val sendToDeviceTask: SendToDeviceTask, private val sendToDeviceTask: SendToDeviceTask,
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore private val cryptoStore: IMXCryptoStore,
) { ) {
/** /**

View File

@ -17,11 +17,16 @@
package org.matrix.android.sdk.internal.database package org.matrix.android.sdk.internal.database
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@ -34,7 +39,7 @@ import javax.inject.Inject
internal class EventInsertLiveObserver @Inject constructor( internal class EventInsertLiveObserver @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration, @SessionDatabase realmConfiguration: RealmConfiguration,
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor> private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
) : ) :
RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) { RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
@ -50,48 +55,90 @@ internal class EventInsertLiveObserver @Inject constructor(
if (!results.isLoaded || results.isEmpty()) { if (!results.isLoaded || results.isEmpty()) {
return@withLock return@withLock
} }
val idsToDeleteAfterProcess = ArrayList<String>() val eventsToProcess = ArrayList<EventInsertEntity>(results.size)
val filteredEvents = ArrayList<EventInsertEntity>(results.size) val eventsToIgnore = ArrayList<EventInsertEntity>(results.size)
Timber.v("EventInsertEntity updated with ${results.size} results in db") Timber.v("EventInsertEntity updated with ${results.size} results in db")
results.forEach { results.forEach {
if (shouldProcess(it)) { // don't use copy from realm over there
// don't use copy from realm over there val copiedEvent = EventInsertEntity(
val copiedEvent = EventInsertEntity( eventId = it.eventId,
eventId = it.eventId, eventType = it.eventType
eventType = it.eventType ).apply {
).apply { insertType = it.insertType
insertType = it.insertType }
}
filteredEvents.add(copiedEvent) if (shouldProcess(it)) {
eventsToProcess.add(copiedEvent)
} else {
eventsToIgnore.add(copiedEvent)
} }
idsToDeleteAfterProcess.add(it.eventId)
} }
awaitTransaction(realmConfiguration) { realm -> awaitTransaction(realmConfiguration) { realm ->
Timber.v("##Transaction: There are ${filteredEvents.size} events to process ") Timber.v("##Transaction: There are ${eventsToProcess.size} events to process")
filteredEvents.forEach { eventInsert ->
val idsToDeleteAfterProcess = ArrayList<String>()
val idsOfEncryptedEvents = ArrayList<String>()
val getAndTriageEvent: (EventInsertEntity) -> Event? = { eventInsert ->
val eventId = eventInsert.eventId val eventId = eventInsert.eventId
val event = EventEntity.where(realm, eventId).findFirst() val event = getEvent(realm, eventId)
if (event == null) { if (event?.getClearType() == EventType.ENCRYPTED) {
Timber.v("Event $eventId not found") idsOfEncryptedEvents.add(eventId)
} else {
idsToDeleteAfterProcess.add(eventId)
}
event
}
eventsToProcess.forEach { eventInsert ->
val eventId = eventInsert.eventId
val event = getAndTriageEvent(eventInsert)
if (event != null && canProcessEvent(event)) {
processors.filter {
it.shouldProcess(eventId, event.getClearType(), eventInsert.insertType)
}.forEach {
it.process(realm, event)
}
} else {
Timber.v("Cannot process event with id $eventId")
return@forEach return@forEach
} }
val domainEvent = event.asDomain()
processors.filter {
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
}.forEach {
it.process(realm, domainEvent)
}
} }
eventsToIgnore.forEach { getAndTriageEvent(it) }
realm.where(EventInsertEntity::class.java) realm.where(EventInsertEntity::class.java)
.`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray()) .`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray())
.findAll() .findAll()
.deleteAllFromRealm() .deleteAllFromRealm()
// make the encrypted events not processable: they will be processed again after decryption
realm.where(EventInsertEntity::class.java)
.`in`(EventInsertEntityFields.EVENT_ID, idsOfEncryptedEvents.toTypedArray())
.findAll()
.forEach { it.canBeProcessed = false }
} }
processors.forEach { it.onPostProcess() } processors.forEach { it.onPostProcess() }
} }
} }
} }
private fun getEvent(realm: Realm, eventId: String): Event? {
val event = EventEntity.where(realm, eventId).findFirst()
if (event == null) {
Timber.v("Event $eventId not found")
}
return event?.asDomain()
}
private fun canProcessEvent(event: Event): Boolean {
// event should be either not encrypted or if encrypted it should contain relatesTo content
return event.getClearType() != EventType.ENCRYPTED ||
event.content.toModel<EncryptedEventContent>()?.relatesTo != null
}
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
return processors.any { return processors.any {
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType) it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)

View File

@ -64,6 +64,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048
import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -72,7 +73,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 47L, schemaVersion = 48L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -129,5 +130,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 45) MigrateSessionTo045(realm).perform() if (oldVersion < 45) MigrateSessionTo045(realm).perform()
if (oldVersion < 46) MigrateSessionTo046(realm).perform() if (oldVersion < 46) MigrateSessionTo046(realm).perform()
if (oldVersion < 47) MigrateSessionTo047(realm).perform() if (oldVersion < 47) MigrateSessionTo047(realm).perform()
if (oldVersion < 48) MigrateSessionTo048(realm).perform()
} }
} }

View File

@ -30,7 +30,8 @@ internal object PollResponseAggregatedSummaryEntityMapper {
closedTime = entity.closedTime, closedTime = entity.closedTime,
localEchos = entity.sourceLocalEchoEvents.toList(), localEchos = entity.sourceLocalEchoEvents.toList(),
sourceEvents = entity.sourceEvents.toList(), sourceEvents = entity.sourceEvents.toList(),
nbOptions = entity.nbOptions nbOptions = entity.nbOptions,
encryptedRelatedEventIds = entity.encryptedRelatedEventIds.toList(),
) )
} }
@ -40,7 +41,8 @@ internal object PollResponseAggregatedSummaryEntityMapper {
nbOptions = model.nbOptions, nbOptions = model.nbOptions,
closedTime = model.closedTime, closedTime = model.closedTime,
sourceEvents = RealmList<String>().apply { addAll(model.sourceEvents) }, sourceEvents = RealmList<String>().apply { addAll(model.sourceEvents) },
sourceLocalEchoEvents = RealmList<String>().apply { addAll(model.localEchos) } sourceLocalEchoEvents = RealmList<String>().apply { addAll(model.localEchos) },
encryptedRelatedEventIds = RealmList<String>().apply { addAll(model.encryptedRelatedEventIds) },
) )
} }
} }

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
/**
* Adding a new field in poll summary to keep track of non decrypted related events.
*/
internal class MigrateSessionTo048(realm: DynamicRealm) : RealmMigrator(realm, 48) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("PollResponseAggregatedSummaryEntity")
?.addRealmListField(PollResponseAggregatedSummaryEntityFields.ENCRYPTED_RELATED_EVENT_IDS.`$`, String::class.java)
}
}

View File

@ -27,7 +27,7 @@ internal open class EventInsertEntity(
var eventType: String = "", var eventType: String = "",
/** /**
* This flag will be used to filter EventInsertEntity in EventInsertLiveObserver. * This flag will be used to filter EventInsertEntity in EventInsertLiveObserver.
* Currently it's set to false when the event content is encrypted. * Currently it's set to false after an event with encrypted content has been processed.
*/ */
var canBeProcessed: Boolean = true var canBeProcessed: Boolean = true
) : RealmObject() { ) : RealmObject() {

View File

@ -33,7 +33,9 @@ internal open class PollResponseAggregatedSummaryEntity(
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
var sourceEvents: RealmList<String> = RealmList(), var sourceEvents: RealmList<String> = RealmList(),
var sourceLocalEchoEvents: RealmList<String> = RealmList() var sourceLocalEchoEvents: RealmList<String> = RealmList(),
// list of related event ids which are encrypted due to decryption failure
var encryptedRelatedEventIds: RealmList<String> = RealmList(),
) : RealmObject() { ) : RealmObject() {
companion object companion object

View File

@ -72,7 +72,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
SpaceParentSummaryEntity::class, SpaceParentSummaryEntity::class,
UserPresenceEntity::class, UserPresenceEntity::class,
ThreadSummaryEntity::class, ThreadSummaryEntity::class,
ThreadListPageEntity::class ThreadListPageEntity::class,
] ]
) )
internal class SessionRealmModule internal class SessionRealmModule

View File

@ -20,7 +20,6 @@ import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@ -32,10 +31,9 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse
.equalTo(EventEntityFields.ROOM_ID, roomId) .equalTo(EventEntityFields.ROOM_ID, roomId)
.findFirst() .findFirst()
return if (eventEntity == null) { return if (eventEntity == null) {
val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = true)
val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply { insertEntity.insertType = insertType
this.insertType = insertType
}
realm.insert(insertEntity) realm.insert(insertEntity)
// copy this event entity and return it // copy this event entity and return it
realm.copyToRealm(this) realm.copyToRealm(this)

View File

@ -61,6 +61,7 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
@ -73,6 +74,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
private val pollAggregationProcessor: PollAggregationProcessor, private val pollAggregationProcessor: PollAggregationProcessor,
private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor,
private val editValidator: EventEditValidator, private val editValidator: EventEditValidator,
private val clock: Clock, private val clock: Clock,
) : EventInsertLiveProcessor { ) : EventInsertLiveProcessor {
@ -140,6 +142,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
handleReaction(realm, event, roomId, isLocalEcho) handleReaction(realm, event, roomId, isLocalEcho)
} }
EventType.ENCRYPTED -> {
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
processEncryptedContent(
encryptedEventContent = encryptedEventContent,
realm = realm,
event = event,
roomId = roomId,
isLocalEcho = isLocalEcho,
)
}
EventType.MESSAGE -> { EventType.MESSAGE -> {
if (event.unsignedData?.relations?.annotations != null) { if (event.unsignedData?.relations?.annotations != null) {
Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}") Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
@ -170,32 +182,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
} }
} }
} }
// As for now Live event processors are not receiving UTD events.
// They will get an update if the event is decrypted later
EventType.ENCRYPTED -> {
// Relation type is in clear, it might be possible to do some things?
// Notice that if the event is decrypted later, process be called again
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
when (encryptedEventContent?.relatesTo?.type) {
RelationType.REPLACE -> {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace!
handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
}
RelationType.RESPONSE -> {
// can we / should we do we something for UTD response??
Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
}
RelationType.REFERENCE -> {
// can we / should we do we something for UTD reference??
Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
}
RelationType.ANNOTATION -> {
// can we / should we do we something for UTD annotation??
Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
}
}
}
EventType.REDACTION -> { EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
?: return ?: return
@ -250,6 +236,36 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
} }
} }
private fun processEncryptedContent(
encryptedEventContent: EncryptedEventContent?,
realm: Realm,
event: Event,
roomId: String,
isLocalEcho: Boolean,
) {
when (encryptedEventContent?.relatesTo?.type) {
RelationType.REPLACE -> {
Timber.w("## UTD replace in room $roomId for event ${event.eventId}")
}
RelationType.RESPONSE -> {
Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
}
RelationType.REFERENCE -> {
Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
encryptedReferenceAggregationProcessor.handle(
realm = realm,
event = event,
isLocalEcho = isLocalEcho,
relatedEventId = encryptedEventContent.relatesTo.eventId,
)
}
RelationType.ANNOTATION -> {
Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
}
else -> Unit
}
}
// OPT OUT serer aggregation until API mature enough // OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e

View File

@ -155,6 +155,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
) )
aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent()) aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent())
event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
return true return true
} }
@ -180,6 +182,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
} }
event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) }
if (!isLocalEcho) { if (!isLocalEcho) {
ensurePollIsFullyAggregated(roomId, pollEventId) ensurePollIsFullyAggregated(roomId, pollEventId)
} }
@ -226,4 +230,10 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
fetchPollResponseEventsTask.execute(params) fetchPollResponseEventsTask.execute(params)
} }
} }
private fun removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity: PollResponseAggregatedSummaryEntity, eventId: String) {
if (aggregatedPollSummaryEntity.encryptedRelatedEventIds.contains(eventId)) {
aggregatedPollSummaryEntity.encryptedRelatedEventIds.remove(eventId)
}
}
} }

View File

@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
interface PollAggregationProcessor { internal interface PollAggregationProcessor {
/** /**
* Poll start events don't need to be processed by the aggregator. * Poll start events don't need to be processed by the aggregator.
* This function will only handle if the poll is edited and will update the poll summary entity. * This function will only handle if the poll is edited and will update the poll summary entity.

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.aggregation.utd
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields
import javax.inject.Inject
internal class EncryptedReferenceAggregationProcessor @Inject constructor() {
fun handle(
realm: Realm,
event: Event,
isLocalEcho: Boolean,
relatedEventId: String?
): Boolean {
return if (isLocalEcho || relatedEventId.isNullOrEmpty()) {
false
} else {
handlePollReference(realm = realm, event = event, relatedEventId = relatedEventId)
true
}
}
private fun handlePollReference(
realm: Realm,
event: Event,
relatedEventId: String
) {
event.eventId?.let { eventId ->
val existingRelatedPoll = getPollSummaryWithEventId(realm, relatedEventId)
if (eventId !in existingRelatedPoll?.encryptedRelatedEventIds.orEmpty()) {
existingRelatedPoll?.encryptedRelatedEventIds?.add(eventId)
}
}
}
private fun getPollSummaryWithEventId(realm: Realm, eventId: String): PollResponseAggregatedSummaryEntity? {
return realm.where(PollResponseAggregatedSummaryEntity::class.java)
.containsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, eventId)
.findFirst()
}
}

View File

@ -0,0 +1,132 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room
import io.mockk.every
import io.mockk.mockk
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
import org.matrix.android.sdk.test.fakes.FakeClock
import org.matrix.android.sdk.test.fakes.FakeRealm
import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
import org.matrix.android.sdk.test.fakes.givenEqualTo
import org.matrix.android.sdk.test.fakes.givenFindFirst
import org.matrix.android.sdk.test.fakes.internal.FakeEventEditValidator
import org.matrix.android.sdk.test.fakes.internal.FakeLiveLocationAggregationProcessor
import org.matrix.android.sdk.test.fakes.internal.FakePollAggregationProcessor
import org.matrix.android.sdk.test.fakes.internal.FakeSessionManager
import org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd.FakeEncryptedReferenceAggregationProcessor
private const val A_ROOM_ID = "room-id"
private const val AN_EVENT_ID = "event-id"
internal class EventRelationsAggregationProcessorTest {
private val fakeStateEventDataSource = FakeStateEventDataSource()
private val fakeSessionManager = FakeSessionManager()
private val fakeLiveLocationAggregationProcessor = FakeLiveLocationAggregationProcessor()
private val fakePollAggregationProcessor = FakePollAggregationProcessor()
private val fakeEncryptedReferenceAggregationProcessor = FakeEncryptedReferenceAggregationProcessor()
private val fakeEventEditValidator = FakeEventEditValidator()
private val fakeClock = FakeClock()
private val fakeRealm = FakeRealm()
private val encryptedEventRelationsAggregationProcessor = EventRelationsAggregationProcessor(
userId = "userId",
stateEventDataSource = fakeStateEventDataSource.instance,
sessionId = "sessionId",
sessionManager = fakeSessionManager.instance,
liveLocationAggregationProcessor = fakeLiveLocationAggregationProcessor.instance,
pollAggregationProcessor = fakePollAggregationProcessor.instance,
encryptedReferenceAggregationProcessor = fakeEncryptedReferenceAggregationProcessor.instance,
editValidator = fakeEventEditValidator.instance,
clock = fakeClock,
)
@Test
fun `given an encrypted reference event when process then reference is processed`() {
// Given
val anEvent = givenAnEvent(
eventId = AN_EVENT_ID,
roomId = A_ROOM_ID,
eventType = EventType.ENCRYPTED,
)
val relatedEventId = "related-event-id"
val encryptedEventContent = givenEncryptedEventContent(
relationType = RelationType.REFERENCE,
relatedEventId = relatedEventId,
)
every { anEvent.content } returns encryptedEventContent.toContent()
val resultOfReferenceProcess = false
fakeEncryptedReferenceAggregationProcessor.givenHandleReturns(resultOfReferenceProcess)
givenEventAnnotationsSummary(roomId = A_ROOM_ID, eventId = AN_EVENT_ID, annotationsSummary = null)
// When
encryptedEventRelationsAggregationProcessor.process(
realm = fakeRealm.instance,
event = anEvent,
)
// Then
fakeEncryptedReferenceAggregationProcessor.verifyHandle(
realm = fakeRealm.instance,
event = anEvent,
isLocalEcho = false,
relatedEventId = relatedEventId,
)
}
private fun givenAnEvent(
eventId: String,
roomId: String?,
eventType: String,
): Event {
return mockk<Event>().also {
every { it.eventId } returns eventId
every { it.roomId } returns roomId
every { it.getClearType() } returns eventType
}
}
private fun givenEncryptedEventContent(relationType: String, relatedEventId: String): EncryptedEventContent {
val relationContent = RelationDefaultContent(
eventId = relatedEventId,
type = relationType,
)
return EncryptedEventContent(
relatesTo = relationContent,
)
}
private fun givenEventAnnotationsSummary(
roomId: String,
eventId: String,
annotationsSummary: EventAnnotationsSummaryEntity?
) {
fakeRealm.givenWhere<EventAnnotationsSummaryEntity>()
.givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
.givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
.givenFindFirst(annotationsSummary)
}
}

View File

@ -25,6 +25,8 @@ import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldContain
import org.amshove.kluent.shouldNotContain
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -105,6 +107,24 @@ class DefaultPollAggregationProcessorTest {
pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeTrue() pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeTrue()
} }
@Test
fun `given a poll response event with a reference, when processing, then event id is removed from encrypted events list`() {
// Given
val anotherEventId = "other-event-id"
val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId)
)
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity
// When
val result = pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT)
// Then
result.shouldBeTrue()
pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID)
pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId)
}
@Test @Test
fun `given a poll response event after poll is closed, when processing, then is ignored and returns false`() { fun `given a poll response event after poll is closed, when processing, then is ignored and returns false`() {
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply { every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply {
@ -132,12 +152,33 @@ class DefaultPollAggregationProcessorTest {
// Given // Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this every { fakeTaskExecutor.instance.executorScope } returns this
// When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true) val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
// When
val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
// Then // Then
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() result.shouldBeTrue()
}
@Test
fun `given a poll end event, when processing, then event id is removed from encrypted events list`() = runTest {
// Given
val anotherEventId = "other-event-id"
val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId)
)
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity
every { fakeTaskExecutor.instance.executorScope } returns this
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
// When
val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
// Then
result.shouldBeTrue()
pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID)
pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId)
} }
@Test @Test
@ -145,12 +186,13 @@ class DefaultPollAggregationProcessorTest {
// Given // Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
every { fakeTaskExecutor.instance.executorScope } returns this every { fakeTaskExecutor.instance.executorScope } returns this
// When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false) val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
// When
val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT)
// Then // Then
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() result.shouldBeTrue()
} }
@Test @Test

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.aggregation.utd
import io.mockk.every
import io.mockk.mockk
import io.realm.RealmList
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldContain
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields
import org.matrix.android.sdk.test.fakes.FakeRealm
import org.matrix.android.sdk.test.fakes.givenContainsValue
import org.matrix.android.sdk.test.fakes.givenFindFirst
internal class EncryptedReferenceAggregationProcessorTest {
private val fakeRealm = FakeRealm()
private val encryptedReferenceAggregationProcessor = EncryptedReferenceAggregationProcessor()
@Test
fun `given local echo when process then result is false`() {
// Given
val anEvent = mockk<Event>()
val isLocalEcho = true
val relatedEventId = "event-id"
// When
val result = encryptedReferenceAggregationProcessor.handle(
realm = fakeRealm.instance,
event = anEvent,
isLocalEcho = isLocalEcho,
relatedEventId = relatedEventId,
)
// Then
result.shouldBeFalse()
}
@Test
fun `given invalid event id when process then result is false`() {
// Given
val anEvent = mockk<Event>()
val isLocalEcho = false
// When
val result1 = encryptedReferenceAggregationProcessor.handle(
realm = fakeRealm.instance,
event = anEvent,
isLocalEcho = isLocalEcho,
relatedEventId = null,
)
val result2 = encryptedReferenceAggregationProcessor.handle(
realm = fakeRealm.instance,
event = anEvent,
isLocalEcho = isLocalEcho,
relatedEventId = "",
)
// Then
result1.shouldBeFalse()
result2.shouldBeFalse()
}
@Test
fun `given related event id of an existing poll when process then result is true and event id is stored in poll summary`() {
// Given
val anEventId = "event-id"
val anEvent = givenAnEvent(anEventId)
val isLocalEcho = false
val relatedEventId = "related-event-id"
val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity(
encryptedRelatedEventIds = RealmList(),
)
fakeRealm.givenWhere<PollResponseAggregatedSummaryEntity>()
.givenContainsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, relatedEventId)
.givenFindFirst(pollResponseAggregatedSummaryEntity)
// When
val result = encryptedReferenceAggregationProcessor.handle(
realm = fakeRealm.instance,
event = anEvent,
isLocalEcho = isLocalEcho,
relatedEventId = relatedEventId,
)
// Then
result.shouldBeTrue()
pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anEventId)
}
@Test
fun `given related event id but no existing related poll when process then result is true and event id is not stored`() {
// Given
val anEventId = "event-id"
val anEvent = givenAnEvent(anEventId)
val isLocalEcho = false
val relatedEventId = "related-event-id"
fakeRealm.givenWhere<PollResponseAggregatedSummaryEntity>()
.givenContainsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, relatedEventId)
.givenFindFirst(null)
// When
val result = encryptedReferenceAggregationProcessor.handle(
realm = fakeRealm.instance,
event = anEvent,
isLocalEcho = isLocalEcho,
relatedEventId = relatedEventId,
)
// Then
result.shouldBeTrue()
}
private fun givenAnEvent(eventId: String): Event {
return mockk<Event>().also {
every { it.eventId } returns eventId
}
}
}

View File

@ -117,6 +117,14 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenIn(
return this return this
} }
inline fun <reified T : RealmModel> RealmQuery<T>.givenContainsValue(
fieldName: String,
value: String,
): RealmQuery<T> {
every { containsValue(fieldName, value) } returns this
return this
}
/** /**
* Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked. * Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked.
*/ */

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.test.fakes.internal
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.EventEditValidator
internal class FakeEventEditValidator {
val instance: EventEditValidator = mockk()
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.test.fakes.internal
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
internal class FakeLiveLocationAggregationProcessor {
val instance: LiveLocationAggregationProcessor = mockk()
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.test.fakes.internal
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
internal class FakePollAggregationProcessor {
val instance: PollAggregationProcessor = mockk()
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor
internal class FakeEncryptedReferenceAggregationProcessor {
val instance: EncryptedReferenceAggregationProcessor = mockk()
fun givenHandleReturns(result: Boolean) {
every { instance.handle(any(), any(), any(), any()) } returns result
}
fun verifyHandle(
realm: Realm,
event: Event,
isLocalEcho: Boolean,
relatedEventId: String?,
) {
verify { instance.handle(realm, event, isLocalEcho, relatedEventId) }
}
}

View File

@ -83,9 +83,14 @@ class PollItemViewStateFactory @Inject constructor(
totalVotes: Int, totalVotes: Int,
winnerVoteCount: Int?, winnerVoteCount: Int?,
): PollViewState { ): PollViewState {
val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
} else {
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes)
}
return PollViewState( return PollViewState(
question = question, question = question,
votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes), votesStatus = totalVotesText,
canVote = false, canVote = false,
optionViewStates = pollCreationInfo?.answers?.map { answer -> optionViewStates = pollCreationInfo?.answers?.map { answer ->
val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "")
@ -126,9 +131,14 @@ class PollItemViewStateFactory @Inject constructor(
pollResponseSummary: PollResponseData?, pollResponseSummary: PollResponseData?,
totalVotes: Int totalVotes: Int
): PollViewState { ): PollViewState {
val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) {
stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll)
} else {
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes)
}
return PollViewState( return PollViewState(
question = question, question = question,
votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes), votesStatus = totalVotesText,
canVote = true, canVote = true,
optionViewStates = pollCreationInfo?.answers?.map { answer -> optionViewStates = pollCreationInfo?.answers?.map { answer ->
val isMyVote = pollResponseSummary?.myVote == answer.id val isMyVote = pollResponseSummary?.myVote == answer.id
@ -144,7 +154,11 @@ class PollItemViewStateFactory @Inject constructor(
) )
} }
private fun createReadyPollViewState(question: String, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState { private fun createReadyPollViewState(
question: String,
pollCreationInfo: PollCreationInfo?,
totalVotes: Int
): PollViewState {
val totalVotesText = if (totalVotes == 0) { val totalVotesText = if (totalVotes == 0) {
stringProvider.getString(R.string.poll_no_votes_cast) stringProvider.getString(R.string.poll_no_votes_cast)
} else { } else {

View File

@ -44,7 +44,8 @@ class PollResponseDataFactory @Inject constructor(
) )
}, },
winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0, winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
totalVotes = it.aggregatedContent?.totalVotes ?: 0 totalVotes = it.aggregatedContent?.totalVotes ?: 0,
hasEncryptedRelatedEvents = it.encryptedRelatedEventIds.isNotEmpty(),
) )
} }
} }

View File

@ -90,7 +90,8 @@ data class PollResponseData(
val votes: Map<String, PollVoteSummaryData>?, val votes: Map<String, PollVoteSummaryData>?,
val totalVotes: Int = 0, val totalVotes: Int = 0,
val winnerVoteCount: Int = 0, val winnerVoteCount: Int = 0,
val isClosed: Boolean = false val isClosed: Boolean = false,
val hasEncryptedRelatedEvents: Boolean = false,
) : Parcelable { ) : Parcelable {
fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId) fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId)

View File

@ -131,6 +131,24 @@ class PollItemViewStateFactoryTest {
) )
} }
@Test
fun `given a sent poll state with some decryption error when poll is closed then warning message is displayed`() {
// Given
val stringProvider = FakeStringProvider()
val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasEncryptedRelatedEvents = true)
val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary)
// When
val pollViewState = pollItemViewStateFactory.create(
pollContent = A_POLL_CONTENT,
informationData = closedPollInformationData,
)
// Then
pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll)
}
@Test @Test
fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() { fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() {
val stringProvider = FakeStringProvider() val stringProvider = FakeStringProvider()
@ -193,6 +211,34 @@ class PollItemViewStateFactoryTest {
) )
} }
@Test
fun `given a sent poll with decryption failure when my vote exists then a warning message is displayed`() {
// Given
val stringProvider = FakeStringProvider()
val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
val votedPollData = A_POLL_RESPONSE_DATA.copy(
totalVotes = 1,
myVote = A_POLL_OPTION_IDS[0],
votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)),
hasEncryptedRelatedEvents = true,
)
val disclosedPollContent = A_POLL_CONTENT.copy(
unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy(
kind = PollType.DISCLOSED_UNSTABLE
),
)
val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData)
// When
val pollViewState = pollItemViewStateFactory.create(
pollContent = disclosedPollContent,
informationData = votedInformationData,
)
// Then
pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll)
}
@Test @Test
fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() { fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() {
val stringProvider = FakeStringProvider() val stringProvider = FakeStringProvider()