porting messenger tests to the engine module

This commit is contained in:
Adam Brown 2022-10-12 19:57:16 +01:00
parent 6e076e7c9f
commit 8f1d8cdcc1
18 changed files with 140 additions and 120 deletions

View File

@ -1,8 +1,12 @@
plugins {
id 'kotlin'
id 'java-test-fixtures'
}
dependencies {
api Dependencies.mavenCentral.kotlinCoroutinesCore
api project(":matrix:common")
kotlinFixtures(it)
testFixturesImplementation(testFixtures(project(":matrix:common")))
}

View File

@ -10,10 +10,8 @@ import java.io.InputStream
interface ChatEngine : TaskRunner {
fun directory(): Flow<DirectoryState>
fun invites(): Flow<InviteState>
suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState>
fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState>
suspend fun login(request: LoginRequest): LoginResult
@ -49,7 +47,6 @@ interface TaskRunner {
}
data class ChatEngineTask(val type: String, val jsonPayload: String)
interface MediaDecrypter {

View File

@ -0,0 +1,31 @@
package fixture
import app.dapk.st.engine.*
import app.dapk.st.matrix.common.*
fun aMessengerState(
self: UserId = aUserId(),
roomState: RoomState,
typing: Typing? = null
) = MessengerState(self, roomState, typing)
fun aRoomOverview(
roomId: RoomId = aRoomId(),
roomCreationUtc: Long = 0L,
roomName: String? = null,
roomAvatarUrl: AvatarUrl? = null,
lastMessage: RoomOverview.LastMessage? = null,
isGroup: Boolean = false,
readMarker: EventId? = null,
isEncrypted: Boolean = false,
) = RoomOverview(roomId, roomCreationUtc, roomName, roomAvatarUrl, lastMessage, isGroup, readMarker, isEncrypted)
fun anEncryptedRoomMessageEvent(
eventId: EventId = anEventId(),
utcTimestamp: Long = 0L,
content: String = "encrypted-content",
author: RoomMember = aRoomMember(),
meta: MessageMeta = MessageMeta.FromServer,
edited: Boolean = false,
redacted: Boolean = false,
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited, redacted)

View File

@ -1,3 +1,5 @@
package test
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow

View File

@ -3,6 +3,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.amshove.kluent.internal.assertEquals
import test.ExpectTestScope
import test.FlowTestObserver
@Suppress("UNCHECKED_CAST")
internal class ViewModelTestScopeImpl(

View File

@ -13,11 +13,10 @@ dependencies {
kotlinTest(it)
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
androidImportFixturesWorkaround(project, project(":matrix:services:message"))
androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:store"))
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
androidImportFixturesWorkaround(project, project(":chat-engine"))
}

View File

@ -2,58 +2,38 @@ package app.dapk.st.messenger
import ViewModelTest
import app.dapk.st.core.Lce
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.message.internal.ImageContentReader
import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.matrix.sync.SyncService
import fake.FakeCredentialsStore
import app.dapk.st.engine.*
import app.dapk.st.matrix.common.*
import fake.FakeMessageOptionsStore
import fake.FakeRoomStore
import fixture.*
import internalfake.FakeLocalIdFactory
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import org.junit.Test
import test.delegateReturn
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
private const val A_CURRENT_TIMESTAMP = 10000L
private const val READ_RECEIPTS_ARE_DISABLED = true
private val A_ROOM_ID = aRoomId("messenger state room id")
private const val A_MESSAGE_CONTENT = "message content"
private const val A_LOCAL_ID = "local.1111-2222-3333"
private val AN_EVENT_ID = anEventId("state event")
private val A_SELF_ID = aUserId("self")
class FakeChatEngine : ChatEngine by mockk() {
fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn()
}
class MessengerViewModelTest {
private val runViewModelTest = ViewModelTest()
private val fakeMessageService = FakeMessageService()
private val fakeRoomService = FakeRoomService()
private val fakeRoomStore = FakeRoomStore()
private val fakeCredentialsStore = FakeCredentialsStore().also { it.givenCredentials().returns(aUserCredentials(userId = A_SELF_ID)) }
private val fakeObserveTimelineUseCase = FakeObserveTimelineUseCase()
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
private val fakeChatEngine = FakeChatEngine()
private val viewModel = MessengerViewModel(
fakeMessageService,
fakeRoomService,
fakeRoomStore,
fakeCredentialsStore,
fakeObserveTimelineUseCase,
localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance,
imageContentReader = FakeImageContentReader(),
messageOptionsStore = fakeMessageOptionsStore.instance,
clock = fixedClock(A_CURRENT_TIMESTAMP),
fakeChatEngine,
fakeMessageOptionsStore.instance,
factory = runViewModelTest.testMutableStateFactory(),
)
@ -73,10 +53,8 @@ class MessengerViewModelTest {
@Test
fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest {
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED)
fakeRoomStore.expectUnit(times = 2) { it.markRead(A_ROOM_ID) }
fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID, isPrivate = READ_RECEIPTS_ARE_DISABLED) }
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state))
fakeChatEngine.givenMessages(A_ROOM_ID, READ_RECEIPTS_ARE_DISABLED).returns(flowOf(state))
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID, attachments = null))
@ -98,7 +76,7 @@ class MessengerViewModelTest {
@Test
fun `given composer message state when posting send text, then resets composer state and sends message`() = runViewModelTest {
fakeMessageService.expectUnit { it.scheduleMessage(expectEncryptedMessage(A_ROOM_ID, A_LOCAL_ID, A_CURRENT_TIMESTAMP, A_MESSAGE_CONTENT)) }
fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT), aRoomOverview()) }
viewModel.test(initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)).post(MessengerAction.ComposerSendText)
@ -114,9 +92,8 @@ class MessengerViewModelTest {
return aMessageScreenState(roomId, aMessengerState(roomState = roomState), messageContent)
}
private fun expectEncryptedMessage(roomId: RoomId, localId: String, timestamp: Long, messageContent: String): MessageService.Message.TextMessage {
val content = MessageService.Message.Content.TextContent(body = messageContent)
return MessageService.Message.TextMessage(content, sendEncrypted = true, roomId, localId, timestamp)
private fun expectTextMessage(messageContent: String): SendMessage.TextMessage {
return SendMessage.TextMessage(messageContent, reply = null)
}
private fun aMessengerStateWithEvent(eventId: EventId, selfId: UserId) = aRoomStateWithEventId(eventId).toMessengerState(selfId)
@ -135,27 +112,3 @@ fun aMessageScreenState(roomId: RoomId = aRoomId(), roomState: MessengerState, m
roomState = Lce.Content(roomState),
composerState = ComposerState.Text(value = messageContent ?: "", reply = null)
)
fun aMessengerState(
self: UserId = aUserId(),
roomState: RoomState,
typing: SyncService.SyncEvent.Typing? = null
) = MessengerState(self, roomState, typing)
class FakeObserveTimelineUseCase : ObserveTimelineUseCase by mockk() {
fun given(roomId: RoomId, selfId: UserId) = coEvery { this@FakeObserveTimelineUseCase.invoke(roomId, selfId) }.delegateReturn()
}
class FakeMessageService : MessageService by mockk() {
fun givenEchos(roomId: RoomId) = every { localEchos(roomId) }.delegateReturn()
}
class FakeRoomService : RoomService by mockk() {
fun givenFindMember(roomId: RoomId, userId: UserId) = coEvery { findMember(roomId, userId) }.delegateReturn()
}
fun fixedClock(timestamp: Long = 0) = Clock.fixed(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC)
class FakeImageContentReader : ImageContentReader by mockk()

View File

@ -1,4 +1,5 @@
plugins {
id 'java-test-fixtures'
id 'kotlin'
}
@ -20,4 +21,14 @@ dependencies {
implementation project(":matrix:services:device")
implementation project(":matrix:services:crypto")
implementation project(":matrix:services:profile")
kotlinTest(it)
kotlinFixtures(it)
testImplementation(testFixtures(project(":matrix:services:sync")))
testImplementation(testFixtures(project(":matrix:services:message")))
testImplementation(testFixtures(project(":matrix:common")))
testImplementation(testFixtures(project(":core")))
testImplementation(testFixtures(project(":domains:store")))
testImplementation(testFixtures(project(":chat-engine")))
}

View File

@ -46,8 +46,8 @@ class MatrixEngine internal constructor(
override fun directory() = directoryUseCase.value.state()
override fun invites() = inviteUseCase.value.invites()
override suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState> {
return timelineUseCase.value.foo(roomId, isReadReceiptsDisabled = disableReadReceipts)
override fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState> {
return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts)
}
override suspend fun login(request: LoginRequest): LoginResult {

View File

@ -10,9 +10,7 @@ import app.dapk.st.matrix.sync.RoomStore
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.*
class ReadMarkingTimeline(
private val roomStore: RoomStore,
@ -21,11 +19,14 @@ class ReadMarkingTimeline(
private val roomService: RoomService,
) {
suspend fun foo(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow<MessengerState> {
var lastKnownReadEvent: EventId? = null
fun fetch(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow<MessengerState> {
return flow {
val credentials = credentialsStore.credentials()!!
roomStore.markRead(roomId)
return observeTimelineUseCase.invoke(roomId, credentials.userId).distinctUntilChanged().onEach { state ->
emit(credentials)
}.flatMapMerge { credentials ->
var lastKnownReadEvent: EventId? = null
observeTimelineUseCase.invoke(roomId, credentials.userId).distinctUntilChanged().onEach { state ->
state.latestMessageEventFromOthers(self = credentials.userId)?.let {
if (lastKnownReadEvent != it) {
updateRoomReadStateAsync(latestReadEvent = it, state, isReadReceiptsDisabled)
@ -34,6 +35,7 @@ class ReadMarkingTimeline(
}
}
}
}
private suspend fun updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState, isReadReceiptsDisabled: Boolean): Deferred<*> {
return coroutineScope {

View File

@ -1,10 +1,10 @@
package app.dapk.st.messenger
package app.dapk.st.engine
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.sync.MessageMeta
import fake.FakeMetaMapper
import fixture.*
import internalfake.FakeMetaMapper
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
@ -28,7 +28,7 @@ class LocalEchoMapperTest {
eventId = echo.eventId!!,
content = AN_ECHO_CONTENT.content.body,
meta = A_META
)
).engine()
}
@Test
@ -41,24 +41,24 @@ class LocalEchoMapperTest {
eventId = anEventId(echo.localId),
content = AN_ECHO_CONTENT.content.body,
meta = A_META
)
).engine()
}
@Test
fun `when merging with echo then updates meta with the echos meta`() = runWith(localEchoMapper) {
val previousMeta = MessageMeta.LocalEcho("previous", MessageMeta.LocalEcho.State.Sending)
val event = aRoomMessageEvent(meta = previousMeta)
val event = aRoomMessageEvent(meta = previousMeta).engine()
val echo = aLocalEcho()
fakeMetaMapper.given(echo).returns(A_META)
fakeMetaMapper.given(echo).returns(A_META.engine() as app.dapk.st.engine.MessageMeta.LocalEcho)
val result = event.mergeWith(echo)
result shouldBeEqualTo aRoomMessageEvent(meta = A_META)
result shouldBeEqualTo aRoomMessageEvent(meta = A_META).engine()
}
private fun givenEcho(eventId: EventId? = null, localId: String = "", meta: MessageMeta.LocalEcho = A_META): MessageService.LocalEcho {
return aLocalEcho(eventId = eventId, message = aTextMessage(localId = localId)).also {
fakeMetaMapper.given(it).returns(meta)
fakeMetaMapper.given(it).returns(meta.engine() as app.dapk.st.engine.MessageMeta.LocalEcho)
}
}
}

View File

@ -1,11 +1,8 @@
package app.dapk.st.messenger
package app.dapk.st.engine
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.MessageType
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.message.MessageService
import fixture.*
import internalfake.FakeLocalEventMapper
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
@ -18,12 +15,12 @@ private val ANOTHER_ROOM_MESSAGE_EVENT = A_ROOM_MESSAGE_EVENT.copy(eventId = anE
class MergeWithLocalEchosUseCaseTest {
private val fakeLocalEchoMapper = FakeLocalEventMapper()
private val fakeLocalEchoMapper = fake.FakeLocalEventMapper()
private val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(fakeLocalEchoMapper.instance)
@Test
fun `given no local echos, when merging text message, then returns original state`() {
val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT))
val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)).engine()
val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, emptyList())
@ -32,7 +29,7 @@ class MergeWithLocalEchosUseCaseTest {
@Test
fun `given no local echos, when merging events, then returns original ordered by timestamp descending`() {
val roomState = aRoomState(events = listOf(A_ROOM_IMAGE_MESSAGE_EVENT.copy(utcTimestamp = 1500), A_ROOM_MESSAGE_EVENT.copy(utcTimestamp = 1000)))
val roomState = aRoomState(events = listOf(A_ROOM_IMAGE_MESSAGE_EVENT.copy(utcTimestamp = 1500), A_ROOM_MESSAGE_EVENT.copy(utcTimestamp = 1000))).engine()
val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, emptyList())
@ -42,15 +39,15 @@ class MergeWithLocalEchosUseCaseTest {
@Test
fun `given local echo with sending state, when merging then maps to room event with local echo state`() {
val second = createLocalEcho(A_LOCAL_ECHO_EVENT_ID, A_LOCAL_ECHO_BODY, state = MessageService.LocalEcho.State.Sending)
fakeLocalEchoMapper.givenMapping(second, A_ROOM_MEMBER).returns(ANOTHER_ROOM_MESSAGE_EVENT)
val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT))
fakeLocalEchoMapper.givenMapping(second, A_ROOM_MEMBER).returns(ANOTHER_ROOM_MESSAGE_EVENT.engine())
val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)).engine()
val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, listOf(second))
result shouldBeEqualTo roomState.copy(
events = listOf(
A_ROOM_MESSAGE_EVENT,
ANOTHER_ROOM_MESSAGE_EVENT,
A_ROOM_MESSAGE_EVENT.engine(),
ANOTHER_ROOM_MESSAGE_EVENT.engine(),
)
)
}

View File

@ -1,7 +1,6 @@
package app.dapk.st.messenger
package app.dapk.st.engine
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.sync.MessageMeta
import fixture.aLocalEcho
import fixture.aTextMessage
import org.amshove.kluent.shouldBeEqualTo

View File

@ -1,13 +1,15 @@
package app.dapk.st.messenger
package app.dapk.st.engine
import FlowTestObserver
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.matrix.sync.SyncService
import fake.FakeSyncService
import fixture.*
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
@ -16,6 +18,7 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import test.FlowTestObserver
import test.delegateReturn
private val A_ROOM_ID = aRoomId()
@ -47,7 +50,7 @@ class TimelineUseCaseTest {
.test(this)
.assertValues(
listOf(
aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE)
aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE.engine())
)
)
}
@ -57,13 +60,13 @@ class TimelineUseCaseTest {
givenSyncEmission(roomState = A_ROOM_STATE, echos = A_LOCAL_ECHOS_LIST)
fakeRoomService.givenFindMember(A_ROOM_ID, AN_USER_ID).returns(A_ROOM_MEMBER)
fakeMergeWithLocalEchosUseCase.givenMerging(A_ROOM_STATE, A_ROOM_MEMBER, A_LOCAL_ECHOS_LIST).returns(A_MERGED_ROOM_STATE)
fakeMergeWithLocalEchosUseCase.givenMerging(A_ROOM_STATE, A_ROOM_MEMBER, A_LOCAL_ECHOS_LIST).returns(A_MERGED_ROOM_STATE.engine())
timelineUseCase.invoke(A_ROOM_ID, AN_USER_ID)
.test(this)
.assertValues(
listOf(
aMessengerState(self = AN_USER_ID, roomState = A_MERGED_ROOM_STATE)
aMessengerState(self = AN_USER_ID, roomState = A_MERGED_ROOM_STATE.engine())
)
)
}
@ -81,7 +84,11 @@ class TimelineUseCaseTest {
.test(this)
.assertValues(
listOf(
aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE, typing = aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER)))
aMessengerState(
self = AN_USER_ID,
roomState = A_ROOM_STATE.engine(),
typing = aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER)).engine()
)
)
)
}
@ -104,7 +111,7 @@ suspend fun <T> Flow<T>.test(scope: CoroutineScope) = FlowTestObserver(scope, th
class FakeMergeWithLocalEchosUseCase : MergeWithLocalEchosUseCase by mockk() {
fun givenMerging(roomState: RoomState, roomMember: RoomMember, echos: List<MessageService.LocalEcho>) = every {
this@FakeMergeWithLocalEchosUseCase.invoke(roomState, roomMember, echos)
this@FakeMergeWithLocalEchosUseCase.invoke(roomState.engine(), roomMember, echos)
}.delegateReturn()
}
@ -112,3 +119,19 @@ fun aTypingSyncEvent(
roomId: RoomId = aRoomId(),
members: List<RoomMember> = listOf(aRoomMember())
) = SyncService.SyncEvent.Typing(roomId, members)
class FakeMessageService : MessageService by mockk() {
fun givenEchos(roomId: RoomId) = every { localEchos(roomId) }.delegateReturn()
}
class FakeRoomService : RoomService by mockk() {
fun givenFindMember(roomId: RoomId, userId: UserId) = coEvery { findMember(roomId, userId) }.delegateReturn()
}
fun aMessengerState(
self: UserId = aUserId(),
roomState: app.dapk.st.engine.RoomState,
typing: Typing? = null
) = MessengerState(self, roomState, typing)

View File

@ -1,8 +1,8 @@
package internalfake
package fake
import app.dapk.st.engine.LocalEchoMapper
import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.messenger.LocalEchoMapper
import io.mockk.every
import io.mockk.mockk

View File

@ -1,6 +1,6 @@
package internalfake
package fake
import app.dapk.st.messenger.LocalIdFactory
import app.dapk.st.engine.LocalIdFactory
import io.mockk.every
import io.mockk.mockk
import test.delegateReturn

View File

@ -1,7 +1,7 @@
package internalfake
package fake
import app.dapk.st.engine.MetaMapper
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.messenger.MetaMapper
import io.mockk.every
import io.mockk.mockk
import test.delegateReturn

View File

@ -5,11 +5,12 @@ import app.dapk.st.matrix.sync.SyncService
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import test.delegateReturn
class FakeSyncService : SyncService by mockk() {
fun givenStartsSyncing() {
every { startSyncing() }.returns(emptyFlow())
every { startSyncing() }.returns(flowOf(Unit))
}
fun givenRoom(roomId: RoomId) = every { room(roomId) }.delegateReturn()