From bd7b6d6495b1192fbf81ae0ac797354d3650620b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 14 Dec 2022 16:33:27 +0100 Subject: [PATCH] Adding unit test on task to fetch the poll response events --- .../poll/FetchPollResponseEventsTask.kt | 24 +-- .../DefaultFetchPollResponseEventsTaskTest.kt | 163 ++++++++++++++++++ .../sdk/test/fakes/FakeEventDecryptor.kt | 35 ++++ .../android/sdk/test/fakes/FakeRealm.kt | 8 + .../android/sdk/test/fakes/FakeRoomApi.kt | 61 +++++++ 5 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt index 3cb8fc5540..3e70da395d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -16,6 +16,7 @@ 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 @@ -37,7 +38,8 @@ import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject -private const val FETCH_RELATED_EVENTS_LIMIT = 50 +@VisibleForTesting +const val FETCH_RELATED_EVENTS_LIMIT = 50 /** * Task to fetch all the vote events to ensure full aggregation for a given poll. @@ -49,7 +51,6 @@ internal interface FetchPollResponseEventsTask : Task) { monarchy.awaitTransaction { realm -> val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } - val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } + if(eventIdsToCheck.isNotEmpty()) { + val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } - events.filterNot { it.eventId in existingIds } - .map { - val ageLocalTs = clock.epochMillis() - (it.unsignedData?.age ?: 0) - it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = ageLocalTs) - } - .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } + events.filterNot { it.eventId in existingIds } + .map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) } + .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } + } } } @@ -109,8 +109,10 @@ internal class DefaultFetchPollResponseEventsTask @Inject constructor( eventDecryptor.decryptEventAndSaveResult(event, timeline = "") } - event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) + event.ageLocalTs = computeLocalTs(event) return event } + + private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt new file mode 100644 index 0000000000..8d50bac38f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt @@ -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, nextBatch: String?): RelationsResponse { + return RelationsResponse( + chunks = events, + nextBatch = nextBatch, + prevBatch = null, + ) + } + + private fun givenAnEvent( + eventId: String, + isPollResponse: Boolean, + isEncrypted: Boolean, + ): Event { + val event = mockk(relaxed = true) + every { event.eventId } returns eventId + every { event.isPollResponse() } returns isPollResponse + every { event.isEncrypted() } returns isEncrypted + return event + } + + private fun givenExistingEventEntities(eventIdsToCheck: List, existingIds: List) { + val eventEntities = existingIds.map { EventEntity(eventId = it) } + fakeMonarchy.givenWhere() + .givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck) + .givenFindAll(eventEntities) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt new file mode 100644 index 0000000000..f2b62ad3ba --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt @@ -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) } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index afdcf111f8..ba124a86aa 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -109,6 +109,14 @@ inline fun RealmQuery.givenLessThan( return this } +inline fun RealmQuery.givenIn( + fieldName: String, + values: List, +): RealmQuery { + 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. */ diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt new file mode 100644 index 0000000000..68dbbe7ea6 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt @@ -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 + ) + } + } +}