Adding unit test on task to fetch the poll response events

This commit is contained in:
Maxime NATUREL 2022-12-14 16:33:27 +01:00
parent 644803dcf3
commit bd7b6d6495
5 changed files with 280 additions and 11 deletions

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 package org.matrix.android.sdk.internal.session.room.relation.poll
import androidx.annotation.VisibleForTesting
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
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.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 org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject 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. * Task to fetch all the vote events to ensure full aggregation for a given poll.
@ -49,7 +51,6 @@ internal interface FetchPollResponseEventsTask : Task<FetchPollResponseEventsTas
) )
} }
// TODO add unit tests
internal class DefaultFetchPollResponseEventsTask @Inject constructor( internal class DefaultFetchPollResponseEventsTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver, private val globalErrorReceiver: GlobalErrorReceiver,
@ -93,24 +94,25 @@ internal class DefaultFetchPollResponseEventsTask @Inject constructor(
private suspend fun addMissingEventsInDB(roomId: String, events: List<Event>) { private suspend fun addMissingEventsInDB(roomId: String, events: List<Event>) {
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() }
if(eventIdsToCheck.isNotEmpty()) {
val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId }
events.filterNot { it.eventId in existingIds } events.filterNot { it.eventId in existingIds }
.map { .map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) }
val ageLocalTs = clock.epochMillis() - (it.unsignedData?.age ?: 0)
it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = ageLocalTs)
}
.forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) }
} }
} }
}
private suspend fun decryptEventIfNeeded(event: Event): Event { private suspend fun decryptEventIfNeeded(event: Event): Event {
if (event.isEncrypted()) { if (event.isEncrypted()) {
eventDecryptor.decryptEventAndSaveResult(event, timeline = "") eventDecryptor.decryptEventAndSaveResult(event, timeline = "")
} }
event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) event.ageLocalTs = computeLocalTs(event)
return event return event
} }
private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0)
} }

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

@ -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
)
}
}
}