change (member moderation) : clean and add tests on Presenter
This commit is contained in:
parent
b1441e1afd
commit
1ac5674c18
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.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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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