change (member moderation) : clean and add tests on Presenter
This commit is contained in:
parent
b0c95db0de
commit
2ac333bb52
4 changed files with 353 additions and 14 deletions
|
|
@ -16,7 +16,6 @@ import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.features.roommembermoderation.api.ModerationAction
|
import io.element.android.features.roommembermoderation.api.ModerationAction
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||||
|
|
@ -44,8 +43,9 @@ import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomMemberListPresenter @AssistedInject constructor(
|
class RoomMemberListPresenter @Inject constructor(
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
private val roomMemberListDataSource: RoomMemberListDataSource,
|
private val roomMemberListDataSource: RoomMemberListDataSource,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
|
|
|
||||||
|
|
@ -36,4 +36,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,21 +71,19 @@ class RoomMemberModerationPresenter @Inject constructor(
|
||||||
fun handleEvent(event: RoomMemberModerationEvents) {
|
fun handleEvent(event: RoomMemberModerationEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is RoomMemberModerationEvents.ShowActionsForUser -> {
|
is RoomMemberModerationEvents.ShowActionsForUser -> {
|
||||||
coroutineScope.launch {
|
selectedUser = event.user
|
||||||
selectedUser = event.user
|
val member = room.membersStateFlow.value.roomMembers()?.firstOrNull {
|
||||||
val member = room.membersStateFlow.value.roomMembers()?.firstOrNull {
|
it.userId == event.user.userId
|
||||||
it.userId == event.user.userId
|
|
||||||
}
|
|
||||||
moderationActions.value = computeModerationActions(
|
|
||||||
member = member,
|
|
||||||
canKick = canKick.value,
|
|
||||||
canBan = canBan.value,
|
|
||||||
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
moderationActions.value = computeModerationActions(
|
||||||
|
member = member,
|
||||||
|
canKick = canKick.value,
|
||||||
|
canBan = canBan.value,
|
||||||
|
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is RoomMemberModerationEvents.ProcessAction -> {
|
is RoomMemberModerationEvents.ProcessAction -> {
|
||||||
when (val action = event.action) {
|
when (event.action) {
|
||||||
is ModerationAction.DisplayProfile -> Unit
|
is ModerationAction.DisplayProfile -> Unit
|
||||||
is ModerationAction.KickUser -> {
|
is ModerationAction.KickUser -> {
|
||||||
selectedUser = event.targetUser
|
selectedUser = event.targetUser
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,338 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.roommembermoderation.impl
|
||||||
|
|
||||||
|
import app.cash.turbine.TurbineTestContext
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.roommembermoderation.api.ModerationAction
|
||||||
|
import io.element.android.features.roommembermoderation.api.ModerationActionState
|
||||||
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
|
||||||
|
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RoomMemberModerationPresenterTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
private val targetUser = MatrixUser(userId = A_USER_ID)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val room = aJoinedRoom()
|
||||||
|
createRoomMemberModerationPresenter(room = room).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
assertThat(initialState.canKick).isFalse()
|
||||||
|
assertThat(initialState.canBan).isFalse()
|
||||||
|
assertThat(initialState.selectedUser).isNull()
|
||||||
|
assertThat(initialState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
assertThat(initialState.kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
assertThat(initialState.unbanUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
assertThat(initialState.actions).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - show actions when canBan=false, canKick=false`() = runTest {
|
||||||
|
val room = aJoinedRoom(
|
||||||
|
canBan = false,
|
||||||
|
canKick = false,
|
||||||
|
myUserRole = RoomMember.Role.USER,
|
||||||
|
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel)
|
||||||
|
)
|
||||||
|
createRoomMemberModerationPresenter(room = room).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser))
|
||||||
|
skipItems(1)
|
||||||
|
val updatedState = awaitState()
|
||||||
|
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
|
||||||
|
assertThat(updatedState.actions).containsExactly(
|
||||||
|
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - show actions when canBan=true, canKick=true, userRole=Admin and target member is unknown`() = runTest {
|
||||||
|
val room = aJoinedRoom(
|
||||||
|
canBan = true,
|
||||||
|
canKick = true,
|
||||||
|
myUserRole = RoomMember.Role.ADMIN,
|
||||||
|
targetRoomMember = null
|
||||||
|
)
|
||||||
|
createRoomMemberModerationPresenter(room = room).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser))
|
||||||
|
skipItems(2)
|
||||||
|
val updatedState = awaitState()
|
||||||
|
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
|
||||||
|
assertThat(updatedState.actions).containsExactly(
|
||||||
|
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
|
||||||
|
ModerationActionState(action = ModerationAction.KickUser, isEnabled = true),
|
||||||
|
ModerationActionState(action = ModerationAction.BanUser, isEnabled = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `show actions when canBan=true, canKick=true, userRole=Admin and target is User`() = runTest {
|
||||||
|
val room = aJoinedRoom(
|
||||||
|
canBan = true,
|
||||||
|
canKick = true,
|
||||||
|
myUserRole = RoomMember.Role.ADMIN,
|
||||||
|
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel)
|
||||||
|
)
|
||||||
|
createRoomMemberModerationPresenter(room = room).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser))
|
||||||
|
skipItems(2)
|
||||||
|
val updatedState = awaitState()
|
||||||
|
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
|
||||||
|
assertThat(updatedState.actions).containsExactly(
|
||||||
|
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
|
||||||
|
ModerationActionState(action = ModerationAction.KickUser, isEnabled = true),
|
||||||
|
ModerationActionState(action = ModerationAction.BanUser, isEnabled = true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `show actions when canBan=true, canKick=true, userRole=Moderator and target is Admin`() = runTest {
|
||||||
|
val room = aJoinedRoom(
|
||||||
|
canBan = true,
|
||||||
|
canKick = true,
|
||||||
|
myUserRole = RoomMember.Role.MODERATOR,
|
||||||
|
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.ADMIN.powerLevel)
|
||||||
|
)
|
||||||
|
createRoomMemberModerationPresenter(room = room).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser))
|
||||||
|
skipItems(2)
|
||||||
|
val updatedState = awaitState()
|
||||||
|
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
|
||||||
|
assertThat(updatedState.actions).containsExactly(
|
||||||
|
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
|
||||||
|
ModerationActionState(action = ModerationAction.KickUser, isEnabled = false),
|
||||||
|
ModerationActionState(action = ModerationAction.BanUser, isEnabled = false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - process kick action sets confirming state`() = runTest {
|
||||||
|
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(
|
||||||
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
targetUser = targetUser,
|
||||||
|
action = ModerationAction.KickUser
|
||||||
|
)
|
||||||
|
)
|
||||||
|
skipItems(1)
|
||||||
|
val updatedState = awaitState()
|
||||||
|
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
|
||||||
|
assertThat(updatedState.kickUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - process ban action sets confirming state`() = runTest {
|
||||||
|
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(
|
||||||
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
targetUser = targetUser,
|
||||||
|
action = ModerationAction.BanUser
|
||||||
|
)
|
||||||
|
)
|
||||||
|
skipItems(1)
|
||||||
|
val updatedState = awaitState()
|
||||||
|
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
|
||||||
|
assertThat(updatedState.banUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - process unban action sets confirming state`() = runTest {
|
||||||
|
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(
|
||||||
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
targetUser = targetUser,
|
||||||
|
action = ModerationAction.UnbanUser
|
||||||
|
)
|
||||||
|
)
|
||||||
|
skipItems(1)
|
||||||
|
val updatedState = awaitState()
|
||||||
|
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
|
||||||
|
assertThat(updatedState.unbanUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - do kick user with success`() = runTest {
|
||||||
|
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(
|
||||||
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
targetUser = targetUser,
|
||||||
|
action = ModerationAction.KickUser
|
||||||
|
)
|
||||||
|
)
|
||||||
|
skipItems(2)
|
||||||
|
initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason"))
|
||||||
|
skipItems(1)
|
||||||
|
val loadingState = awaitState()
|
||||||
|
assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
val successState = awaitState()
|
||||||
|
assertThat(successState.kickUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
assertThat(successState.selectedUser).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - do ban user with success`() = runTest {
|
||||||
|
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(
|
||||||
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
targetUser = targetUser,
|
||||||
|
action = ModerationAction.BanUser
|
||||||
|
)
|
||||||
|
)
|
||||||
|
skipItems(2)
|
||||||
|
initialState.eventSink(InternalRoomMemberModerationEvents.DoBanUser("Reason"))
|
||||||
|
skipItems(1)
|
||||||
|
val loadingState = awaitState()
|
||||||
|
assertThat(loadingState.banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
val successState = awaitState()
|
||||||
|
assertThat(successState.banUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
assertThat(successState.selectedUser).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - do unban user with success`() = runTest {
|
||||||
|
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(
|
||||||
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
targetUser = targetUser,
|
||||||
|
action = ModerationAction.UnbanUser
|
||||||
|
)
|
||||||
|
)
|
||||||
|
skipItems(2)
|
||||||
|
initialState.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser)
|
||||||
|
skipItems(1)
|
||||||
|
val loadingState = awaitState()
|
||||||
|
assertThat(loadingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
val successState = awaitState()
|
||||||
|
assertThat(successState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
assertThat(successState.selectedUser).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - do kick user with failure`() = runTest {
|
||||||
|
val error = RuntimeException("Test error")
|
||||||
|
val room = aJoinedRoom(
|
||||||
|
kickUserResult = Result.failure(error),
|
||||||
|
)
|
||||||
|
createRoomMemberModerationPresenter(room = room).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(
|
||||||
|
RoomMemberModerationEvents.ProcessAction(
|
||||||
|
targetUser = targetUser,
|
||||||
|
action = ModerationAction.KickUser
|
||||||
|
)
|
||||||
|
)
|
||||||
|
skipItems(2)
|
||||||
|
initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason"))
|
||||||
|
skipItems(1)
|
||||||
|
val loadingState = awaitState()
|
||||||
|
assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
val failureState = awaitState()
|
||||||
|
assertThat(failureState.kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - reset clears all async actions and selected user`() = runTest {
|
||||||
|
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
|
||||||
|
val initialState = awaitState()
|
||||||
|
initialState.eventSink(
|
||||||
|
RoomMemberModerationEvents.ProcessAction(targetUser = targetUser, action = ModerationAction.BanUser)
|
||||||
|
)
|
||||||
|
skipItems(2)
|
||||||
|
initialState.eventSink(InternalRoomMemberModerationEvents.Reset)
|
||||||
|
skipItems(1)
|
||||||
|
val resetState = awaitState()
|
||||||
|
assertThat(resetState.selectedUser).isNull()
|
||||||
|
assertThat(resetState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aJoinedRoom(
|
||||||
|
canKick: Boolean = false,
|
||||||
|
canBan: Boolean = false,
|
||||||
|
myUserRole: RoomMember.Role = RoomMember.Role.USER,
|
||||||
|
kickUserResult: Result<Unit> = Result.success(Unit),
|
||||||
|
banUserResult: Result<Unit> = Result.success(Unit),
|
||||||
|
unBanUserResult: Result<Unit> = Result.success(Unit),
|
||||||
|
targetRoomMember: RoomMember? = null,
|
||||||
|
): JoinedRoom {
|
||||||
|
return FakeJoinedRoom(
|
||||||
|
kickUserResult = { _, _ -> kickUserResult },
|
||||||
|
banUserResult = { _, _ -> banUserResult },
|
||||||
|
unBanUserResult = { _, _ -> unBanUserResult },
|
||||||
|
baseRoom = FakeBaseRoom(
|
||||||
|
canBanResult = { _ -> Result.success(canBan) },
|
||||||
|
canKickResult = { _ -> Result.success(canKick) },
|
||||||
|
userRoleResult = { Result.success(myUserRole) },
|
||||||
|
),
|
||||||
|
).apply {
|
||||||
|
val roomMembers = listOfNotNull(targetRoomMember).toPersistentList()
|
||||||
|
givenRoomMembersState(state = RoomMembersState.Ready(roomMembers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createRoomMemberModerationPresenter(
|
||||||
|
room: JoinedRoom,
|
||||||
|
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
|
): RoomMemberModerationPresenter {
|
||||||
|
return RoomMemberModerationPresenter(
|
||||||
|
room = room,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
analyticsService = analyticsService,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun TurbineTestContext<RoomMemberModerationState>.awaitState(): InternalRoomMemberModerationState {
|
||||||
|
return awaitItem() as InternalRoomMemberModerationState
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue