change (member moderation) : clean and add tests on Presenter

This commit is contained in:
ganfra
2025-05-21 18:31:08 +02:00
parent b0c95db0de
commit 2ac333bb52
4 changed files with 353 additions and 14 deletions

View File

@@ -16,7 +16,6 @@ import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dagger.assisted.AssistedInject
import io.element.android.features.roommembermoderation.api.ModerationAction
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
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.onEach
import kotlinx.coroutines.withContext
import javax.inject.Inject
class RoomMemberListPresenter @AssistedInject constructor(
class RoomMemberListPresenter @Inject constructor(
private val room: JoinedRoom,
private val roomMemberListDataSource: RoomMemberListDataSource,
private val coroutineDispatchers: CoroutineDispatchers,

View File

@@ -36,4 +36,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.analytics.test)
testImplementation(libs.test.robolectric)
testImplementation(libs.androidx.compose.ui.test.junit)
}

View File

@@ -71,21 +71,19 @@ class RoomMemberModerationPresenter @Inject constructor(
fun handleEvent(event: RoomMemberModerationEvents) {
when (event) {
is RoomMemberModerationEvents.ShowActionsForUser -> {
coroutineScope.launch {
selectedUser = event.user
val member = room.membersStateFlow.value.roomMembers()?.firstOrNull {
it.userId == event.user.userId
}
moderationActions.value = computeModerationActions(
member = member,
canKick = canKick.value,
canBan = canBan.value,
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
)
selectedUser = event.user
val member = room.membersStateFlow.value.roomMembers()?.firstOrNull {
it.userId == event.user.userId
}
moderationActions.value = computeModerationActions(
member = member,
canKick = canKick.value,
canBan = canBan.value,
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
)
}
is RoomMemberModerationEvents.ProcessAction -> {
when (val action = event.action) {
when (event.action) {
is ModerationAction.DisplayProfile -> Unit
is ModerationAction.KickUser -> {
selectedUser = event.targetUser

View File

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