Merge pull request #7779 from vector-im/feature/mna/relations-api-poll-end

[Poll] When a poll is ended, use /relations API to ensure poll results are correct (PSG-1013)
This commit is contained in:
Maxime NATUREL 2022-12-16 10:40:39 +01:00 committed by GitHub
commit 648f97ba78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 531 additions and 27 deletions

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

@ -0,0 +1 @@
[Poll] When a poll is ended, use /relations API to ensure poll results are correct

View File

@ -388,7 +388,13 @@ fun Event.isLocationMessage(): Boolean {
} }
} }
fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START.values || getClearType() in EventType.POLL_END.values fun Event.isPoll(): Boolean = isPollStart() || isPollEnd()
fun Event.isPollStart(): Boolean = getClearType() in EventType.POLL_START.values
fun Event.isPollResponse(): Boolean = getClearType() in EventType.POLL_RESPONSE.values
fun Event.isPollEnd(): Boolean = getClearType() in EventType.POLL_END.values
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER

View File

@ -24,10 +24,12 @@ import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
@ -85,6 +87,27 @@ internal class EventDecryptor @Inject constructor(
return internalDecryptEvent(event, timeline) return internalDecryptEvent(event, timeline)
} }
/**
* Decrypt an event and save the result in the given event.
*
* @param event the raw event.
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
*/
suspend fun decryptEventAndSaveResult(event: Event, timeline: String) {
tryOrNull(message = "Unable to decrypt the event") {
decryptEvent(event, timeline)
}
?.let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
)
}
}
/** /**
* Decrypt an event asynchronously. * Decrypt an event asynchronously.
* *

View File

@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
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.events.model.RelationType
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomStrippedState import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
@ -251,7 +250,7 @@ internal interface RoomAPI {
* @param limit max number of Event to retrieve * @param limit max number of Event to retrieve
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}")
suspend fun getRelations( suspend fun getRelationsWithEventType(
@Path("roomId") roomId: String, @Path("roomId") roomId: String,
@Path("eventId") eventId: String, @Path("eventId") eventId: String,
@Path("relationType") relationType: String, @Path("relationType") relationType: String,
@ -262,7 +261,7 @@ internal interface RoomAPI {
): RelationsResponse ): RelationsResponse
/** /**
* Paginate relations for thread events based in normal topological order. * Paginate relations for events based in normal topological order.
* *
* @param roomId the room Id * @param roomId the room Id
* @param eventId the event Id * @param eventId the event Id
@ -272,10 +271,10 @@ internal interface RoomAPI {
* @param limit max number of Event to retrieve * @param limit max number of Event to retrieve
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}") @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
suspend fun getThreadsRelations( suspend fun getRelations(
@Path("roomId") roomId: String, @Path("roomId") roomId: String,
@Path("eventId") eventId: String, @Path("eventId") eventId: String,
@Path("relationType") relationType: String = RelationType.THREAD, @Path("relationType") relationType: String,
@Query("from") from: String? = null, @Query("from") from: String? = null,
@Query("to") to: String? = null, @Query("to") to: String? = null,
@Query("limit") limit: Int? = null @Query("limit") limit: Int? = null

View File

@ -99,6 +99,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
import org.matrix.android.sdk.internal.session.room.relation.poll.DefaultFetchPollResponseEventsTask
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
@ -354,4 +356,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask
@Binds
abstract fun bindFetchPollResponseEventsTask(task: DefaultFetchPollResponseEventsTask): FetchPollResponseEventsTask
} }

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room.aggregation.poll package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.realm.Realm import io.realm.Realm
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session 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
@ -40,9 +41,14 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm
import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import javax.inject.Inject import javax.inject.Inject
class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor { internal class DefaultPollAggregationProcessor @Inject constructor(
private val taskExecutor: TaskExecutor,
private val fetchPollResponseEventsTask: FetchPollResponseEventsTask,
) : PollAggregationProcessor {
override fun handlePollStartEvent(realm: Realm, event: Event): Boolean { override fun handlePollStartEvent(realm: Realm, event: Event): Boolean {
val content = event.getClearContent()?.toModel<MessagePollContent>() val content = event.getClearContent()?.toModel<MessagePollContent>()
@ -174,6 +180,10 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) aggregatedPollSummaryEntity.sourceEvents.add(event.eventId)
} }
if (!isLocalEcho) {
ensurePollIsFullyAggregated(roomId, pollEventId)
}
return true return true
} }
@ -200,4 +210,20 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro
eventAnnotationsSummaryEntity.pollResponseSummary = it eventAnnotationsSummaryEntity.pollResponseSummary = it
} }
} }
/**
* Check that all related votes to a given poll are all retrieved and aggregated.
*/
private fun ensurePollIsFullyAggregated(
roomId: String,
pollEventId: String
) {
taskExecutor.executorScope.launch {
val params = FetchPollResponseEventsTask.Params(
roomId = roomId,
startPollEventId = pollEventId,
)
fetchPollResponseEventsTask.execute(params)
}
}
} }

View File

@ -43,7 +43,7 @@ internal class DefaultFetchEditHistoryTask @Inject constructor(
override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> { override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> {
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId) val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
val response = executeRequest(globalErrorReceiver) { val response = executeRequest(globalErrorReceiver) {
roomAPI.getRelations( roomAPI.getRelationsWithEventType(
roomId = params.roomId, roomId = params.roomId,
eventId = params.eventId, eventId = params.eventId,
relationType = RelationType.REPLACE, relationType = RelationType.REPLACE,

View File

@ -0,0 +1,118 @@
/*
* 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.relation.poll
import androidx.annotation.VisibleForTesting
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.isPollResponse
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject
@VisibleForTesting
const val FETCH_RELATED_EVENTS_LIMIT = 50
/**
* Task to fetch all the vote events to ensure full aggregation for a given poll.
*/
internal interface FetchPollResponseEventsTask : Task<FetchPollResponseEventsTask.Params, Result<Unit>> {
data class Params(
val roomId: String,
val startPollEventId: String,
)
}
internal class DefaultFetchPollResponseEventsTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver,
@SessionDatabase private val monarchy: Monarchy,
private val clock: Clock,
private val eventDecryptor: EventDecryptor,
) : FetchPollResponseEventsTask {
override suspend fun execute(params: FetchPollResponseEventsTask.Params): Result<Unit> = runCatching {
var nextBatch: String? = fetchAndProcessRelatedEventsFrom(params)
while (nextBatch?.isNotEmpty() == true) {
nextBatch = fetchAndProcessRelatedEventsFrom(params, from = nextBatch)
}
}
private suspend fun fetchAndProcessRelatedEventsFrom(params: FetchPollResponseEventsTask.Params, from: String? = null): String? {
val response = getRelatedEvents(params, from)
val filteredEvents = response.chunks
.map { decryptEventIfNeeded(it) }
.filter { it.isPollResponse() }
addMissingEventsInDB(params.roomId, filteredEvents)
return response.nextBatch
}
private suspend fun getRelatedEvents(params: FetchPollResponseEventsTask.Params, from: String? = null): RelationsResponse {
return executeRequest(globalErrorReceiver, canRetry = true) {
roomAPI.getRelations(
roomId = params.roomId,
eventId = params.startPollEventId,
relationType = RelationType.REFERENCE,
from = from,
limit = FETCH_RELATED_EVENTS_LIMIT,
)
}
}
private suspend fun addMissingEventsInDB(roomId: String, events: List<Event>) {
monarchy.awaitTransaction { realm ->
val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() }
if (eventIdsToCheck.isNotEmpty()) {
val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId }
events.filterNot { it.eventId in existingIds }
.map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) }
.forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) }
}
}
}
private suspend fun decryptEventIfNeeded(event: Event): Event {
if (event.isEncrypted()) {
eventDecryptor.decryptEventAndSaveResult(event, timeline = "")
}
event.ageLocalTs = computeLocalTs(event)
return event
}
private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0)
}

View File

@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.events.model.EventType 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.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
@ -102,11 +103,12 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
override suspend fun execute(params: FetchThreadTimelineTask.Params): Result { override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
val response = executeRequest(globalErrorReceiver) { val response = executeRequest(globalErrorReceiver) {
roomAPI.getThreadsRelations( roomAPI.getRelations(
roomId = params.roomId, roomId = params.roomId,
eventId = params.rootThreadEventId, eventId = params.rootThreadEventId,
relationType = RelationType.THREAD,
from = params.from, from = params.from,
limit = params.limit limit = params.limit,
) )
} }

View File

@ -16,8 +16,6 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
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.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
@ -48,18 +46,7 @@ internal class DefaultGetEventTask @Inject constructor(
// Try to decrypt the Event // Try to decrypt the Event
if (event.isEncrypted()) { if (event.isEncrypted()) {
tryOrNull(message = "Unable to decrypt the event") { eventDecryptor.decryptEventAndSaveResult(event, timeline = "")
eventDecryptor.decryptEvent(event, "")
}
?.let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
)
}
} }
event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0)

View File

@ -16,9 +16,13 @@
package org.matrix.android.sdk.internal.session.room.aggregation.poll package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.realm.RealmList import io.realm.RealmList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
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.junit.Before import org.junit.Before
@ -34,6 +38,7 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_EVENT_ID import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_EVENT_ID
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_INVALID_POLL_RESPONSE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_INVALID_POLL_RESPONSE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_BROKEN_POLL_REPLACE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_BROKEN_POLL_REPLACE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_CONTENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REFERENCE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REFERENCE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_EVENT
@ -43,13 +48,22 @@ import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_ROOM_ID import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_ROOM_ID
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1 import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
import org.matrix.android.sdk.test.fakes.FakeFetchPollResponseEventsTask
import org.matrix.android.sdk.test.fakes.FakeRealm import org.matrix.android.sdk.test.fakes.FakeRealm
import org.matrix.android.sdk.test.fakes.FakeTaskExecutor
import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenEqualTo
import org.matrix.android.sdk.test.fakes.givenFindFirst import org.matrix.android.sdk.test.fakes.givenFindFirst
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultPollAggregationProcessorTest { class DefaultPollAggregationProcessorTest {
private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor() private val fakeTaskExecutor = FakeTaskExecutor()
private val fakeFetchPollResponseEventsTask = FakeFetchPollResponseEventsTask()
private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor(
taskExecutor = fakeTaskExecutor.instance,
fetchPollResponseEventsTask = fakeFetchPollResponseEventsTask
)
private val realm = FakeRealm() private val realm = FakeRealm()
private val session = mockk<Session>() private val session = mockk<Session>()
@ -114,16 +128,28 @@ class DefaultPollAggregationProcessorTest {
} }
@Test @Test
fun `given a poll end event, when processing, then is processed and return true`() { fun `given a poll end event, when processing, then is processed and return true`() = runTest {
// 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
// When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true) val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true)
// Then
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
} }
@Test @Test
fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() { fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() = runTest {
// 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
// When
val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false) val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false)
// Then
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue()
} }
@ -135,6 +161,28 @@ class DefaultPollAggregationProcessorTest {
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse() pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse()
} }
@Test
fun `given a non local echo poll end event, when is processed, then ensure to aggregate all poll responses`() = runTest {
// Given
every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity()
val powerLevelsHelper = mockRedactionPowerLevels("another-sender-id", true)
val event = A_POLL_END_EVENT.copy(senderId = "another-sender-id")
every { fakeTaskExecutor.instance.executorScope } returns this
val expectedParams = FetchPollResponseEventsTask.Params(
roomId = A_POLL_END_EVENT.roomId.orEmpty(),
startPollEventId = A_POLL_END_CONTENT.relatesTo?.eventId.orEmpty(),
)
// When
pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event)
advanceUntilIdle()
// Then
coVerify {
fakeFetchPollResponseEventsTask.execute(expectedParams)
}
}
private fun mockEventAnnotationsSummaryEntity() { private fun mockEventAnnotationsSummaryEntity() {
realm.givenWhere<EventAnnotationsSummaryEntity>() realm.givenWhere<EventAnnotationsSummaryEntity>()
.givenFindFirst(EventAnnotationsSummaryEntity()) .givenFindFirst(EventAnnotationsSummaryEntity())

View File

@ -0,0 +1,163 @@
/*
* 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.relation.poll
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.isPollResponse
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.mapper.toEntity
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.EventInsertType
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
import org.matrix.android.sdk.test.fakes.FakeClock
import org.matrix.android.sdk.test.fakes.FakeEventDecryptor
import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
import org.matrix.android.sdk.test.fakes.FakeMonarchy
import org.matrix.android.sdk.test.fakes.FakeRoomApi
import org.matrix.android.sdk.test.fakes.givenFindAll
import org.matrix.android.sdk.test.fakes.givenIn
@OptIn(ExperimentalCoroutinesApi::class)
internal class DefaultFetchPollResponseEventsTaskTest {
private val fakeRoomAPI = FakeRoomApi()
private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver()
private val fakeMonarchy = FakeMonarchy()
private val fakeClock = FakeClock()
private val fakeEventDecryptor = FakeEventDecryptor()
private val defaultFetchPollResponseEventsTask = DefaultFetchPollResponseEventsTask(
roomAPI = fakeRoomAPI.instance,
globalErrorReceiver = fakeGlobalErrorReceiver,
monarchy = fakeMonarchy.instance,
clock = fakeClock,
eventDecryptor = fakeEventDecryptor.instance,
)
@Before
fun setup() {
mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt")
mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt")
mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt")
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a room and a poll when execute then fetch related events and store them in local if needed`() = runTest {
// Given
val aRoomId = "roomId"
val aPollEventId = "eventId"
val params = givenTaskParams(roomId = aRoomId, eventId = aPollEventId)
val aNextBatchToken = "nextBatch"
val anEventId1 = "eventId1"
val anEventId2 = "eventId2"
val anEventId3 = "eventId3"
val anEventId4 = "eventId4"
val event1 = givenAnEvent(eventId = anEventId1, isPollResponse = true, isEncrypted = true)
val event2 = givenAnEvent(eventId = anEventId2, isPollResponse = true, isEncrypted = true)
val event3 = givenAnEvent(eventId = anEventId3, isPollResponse = false, isEncrypted = false)
val event4 = givenAnEvent(eventId = anEventId4, isPollResponse = false, isEncrypted = false)
val firstEvents = listOf(event1, event2)
val secondEvents = listOf(event3, event4)
val firstResponse = givenARelationsResponse(events = firstEvents, nextBatch = aNextBatchToken)
fakeRoomAPI.givenGetRelationsReturns(from = null, relationsResponse = firstResponse)
val secondResponse = givenARelationsResponse(events = secondEvents, nextBatch = null)
fakeRoomAPI.givenGetRelationsReturns(from = aNextBatchToken, relationsResponse = secondResponse)
fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1)
fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2)
fakeClock.givenEpoch(123)
givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1))
val eventEntityToSave = EventEntity(eventId = anEventId2)
every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave
every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave
// When
defaultFetchPollResponseEventsTask.execute(params)
// Then
fakeRoomAPI.verifyGetRelations(
roomId = params.roomId,
eventId = params.startPollEventId,
relationType = RelationType.REFERENCE,
from = null,
limit = FETCH_RELATED_EVENTS_LIMIT
)
fakeRoomAPI.verifyGetRelations(
roomId = params.roomId,
eventId = params.startPollEventId,
relationType = RelationType.REFERENCE,
from = aNextBatchToken,
limit = FETCH_RELATED_EVENTS_LIMIT
)
fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "")
fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "")
// Check we save in DB the event2 which is a non stored poll response
verify {
event2.toEntity(aRoomId, SendState.SYNCED, any())
eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION)
}
}
private fun givenTaskParams(roomId: String, eventId: String) = FetchPollResponseEventsTask.Params(
roomId = roomId,
startPollEventId = eventId,
)
private fun givenARelationsResponse(events: List<Event>, nextBatch: String?): RelationsResponse {
return RelationsResponse(
chunks = events,
nextBatch = nextBatch,
prevBatch = null,
)
}
private fun givenAnEvent(
eventId: String,
isPollResponse: Boolean,
isEncrypted: Boolean,
): Event {
val event = mockk<Event>(relaxed = true)
every { event.eventId } returns eventId
every { event.isPollResponse() } returns isPollResponse
every { event.isEncrypted() } returns isEncrypted
return event
}
private fun givenExistingEventEntities(eventIdsToCheck: List<String>, existingIds: List<String>) {
val eventEntities = existingIds.map { EventEntity(eventId = it) }
fakeMonarchy.givenWhere<EventEntity>()
.givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck)
.givenFindAll(eventEntities)
}
}

View File

@ -0,0 +1,35 @@
/*
* 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
import io.mockk.coJustRun
import io.mockk.coVerify
import io.mockk.mockk
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.EventDecryptor
internal class FakeEventDecryptor {
val instance: EventDecryptor = mockk()
fun givenDecryptEventAndSaveResultSuccess(event: Event) {
coJustRun { instance.decryptEventAndSaveResult(event, any()) }
}
fun verifyDecryptEventAndSaveResult(event: Event, timeline: String) {
coVerify { instance.decryptEventAndSaveResult(event, timeline) }
}
}

View File

@ -0,0 +1,22 @@
/*
* 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
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask
class FakeFetchPollResponseEventsTask : FetchPollResponseEventsTask by mockk(relaxed = true)

View File

@ -109,6 +109,14 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenLessThan(
return this return this
} }
inline fun <reified T : RealmModel> RealmQuery<T>.givenIn(
fieldName: String,
values: List<String>,
): RealmQuery<T> {
every { `in`(fieldName, values.toTypedArray()) } 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,61 @@
/*
* 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
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
internal class FakeRoomApi {
val instance: RoomAPI = mockk()
fun givenGetRelationsReturns(
from: String?,
relationsResponse: RelationsResponse,
) {
coEvery {
instance.getRelations(
roomId = any(),
eventId = any(),
relationType = any(),
from = from,
limit = any()
)
} returns relationsResponse
}
fun verifyGetRelations(
roomId: String,
eventId: String,
relationType: String,
from: String?,
limit: Int,
) {
coVerify {
instance.getRelations(
roomId = roomId,
eventId = eventId,
relationType = relationType,
from = from,
limit = limit
)
}
}
}