Skip to content

Commit 1ac5674

Browse files
committed
change (member moderation) : clean and add tests on Presenter
1 parent b1441e1 commit 1ac5674

File tree

4 files changed

+353
-14
lines changed

4 files changed

+353
-14
lines changed

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import androidx.compose.runtime.produceState
1616
import androidx.compose.runtime.remember
1717
import androidx.compose.runtime.saveable.rememberSaveable
1818
import androidx.compose.runtime.setValue
19-
import dagger.assisted.AssistedInject
2019
import io.element.android.features.roommembermoderation.api.ModerationAction
2120
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
2221
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
@@ -44,8 +43,9 @@ import kotlinx.coroutines.flow.first
4443
import kotlinx.coroutines.flow.launchIn
4544
import kotlinx.coroutines.flow.onEach
4645
import kotlinx.coroutines.withContext
46+
import javax.inject.Inject
4747

48-
class RoomMemberListPresenter @AssistedInject constructor(
48+
class RoomMemberListPresenter @Inject constructor(
4949
private val room: JoinedRoom,
5050
private val roomMemberListDataSource: RoomMemberListDataSource,
5151
private val coroutineDispatchers: CoroutineDispatchers,

features/roommembermoderation/impl/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ dependencies {
3636
testImplementation(projects.libraries.matrix.test)
3737
testImplementation(projects.tests.testutils)
3838
testImplementation(projects.services.analytics.test)
39+
testImplementation(libs.test.robolectric)
40+
testImplementation(libs.androidx.compose.ui.test.junit)
41+
3942
}

features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,19 @@ class RoomMemberModerationPresenter @Inject constructor(
7171
fun handleEvent(event: RoomMemberModerationEvents) {
7272
when (event) {
7373
is RoomMemberModerationEvents.ShowActionsForUser -> {
74-
coroutineScope.launch {
75-
selectedUser = event.user
76-
val member = room.membersStateFlow.value.roomMembers()?.firstOrNull {
77-
it.userId == event.user.userId
78-
}
79-
moderationActions.value = computeModerationActions(
80-
member = member,
81-
canKick = canKick.value,
82-
canBan = canBan.value,
83-
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
84-
)
74+
selectedUser = event.user
75+
val member = room.membersStateFlow.value.roomMembers()?.firstOrNull {
76+
it.userId == event.user.userId
8577
}
78+
moderationActions.value = computeModerationActions(
79+
member = member,
80+
canKick = canKick.value,
81+
canBan = canBan.value,
82+
currentUserMemberPowerLevel = currentUserMemberPowerLevel.value,
83+
)
8684
}
8785
is RoomMemberModerationEvents.ProcessAction -> {
88-
when (val action = event.action) {
86+
when (event.action) {
8987
is ModerationAction.DisplayProfile -> Unit
9088
is ModerationAction.KickUser -> {
9189
selectedUser = event.targetUser
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.roommembermoderation.impl
9+
10+
import app.cash.turbine.TurbineTestContext
11+
import com.google.common.truth.Truth.assertThat
12+
import io.element.android.features.roommembermoderation.api.ModerationAction
13+
import io.element.android.features.roommembermoderation.api.ModerationActionState
14+
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
15+
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
16+
import io.element.android.libraries.architecture.AsyncAction
17+
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
18+
import io.element.android.libraries.matrix.api.room.JoinedRoom
19+
import io.element.android.libraries.matrix.api.room.RoomMember
20+
import io.element.android.libraries.matrix.api.room.RoomMembersState
21+
import io.element.android.libraries.matrix.api.user.MatrixUser
22+
import io.element.android.libraries.matrix.test.A_USER_ID
23+
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
24+
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
25+
import io.element.android.libraries.matrix.test.room.aRoomMember
26+
import io.element.android.services.analytics.api.AnalyticsService
27+
import io.element.android.services.analytics.test.FakeAnalyticsService
28+
import io.element.android.tests.testutils.WarmUpRule
29+
import io.element.android.tests.testutils.test
30+
import io.element.android.tests.testutils.testCoroutineDispatchers
31+
import kotlinx.collections.immutable.toPersistentList
32+
import kotlinx.coroutines.test.TestScope
33+
import kotlinx.coroutines.test.runTest
34+
import org.junit.Rule
35+
import org.junit.Test
36+
37+
class RoomMemberModerationPresenterTest {
38+
39+
@get:Rule
40+
val warmUpRule = WarmUpRule()
41+
42+
private val targetUser = MatrixUser(userId = A_USER_ID)
43+
44+
@Test
45+
fun `present - initial state`() = runTest {
46+
val room = aJoinedRoom()
47+
createRoomMemberModerationPresenter(room = room).test {
48+
val initialState = awaitState()
49+
assertThat(initialState.canKick).isFalse()
50+
assertThat(initialState.canBan).isFalse()
51+
assertThat(initialState.selectedUser).isNull()
52+
assertThat(initialState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
53+
assertThat(initialState.kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
54+
assertThat(initialState.unbanUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
55+
assertThat(initialState.actions).isEmpty()
56+
}
57+
}
58+
59+
@Test
60+
fun `present - show actions when canBan=false, canKick=false`() = runTest {
61+
val room = aJoinedRoom(
62+
canBan = false,
63+
canKick = false,
64+
myUserRole = RoomMember.Role.USER,
65+
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel)
66+
)
67+
createRoomMemberModerationPresenter(room = room).test {
68+
val initialState = awaitState()
69+
initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser))
70+
skipItems(1)
71+
val updatedState = awaitState()
72+
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
73+
assertThat(updatedState.actions).containsExactly(
74+
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
75+
)
76+
}
77+
}
78+
79+
@Test
80+
fun `present - show actions when canBan=true, canKick=true, userRole=Admin and target member is unknown`() = runTest {
81+
val room = aJoinedRoom(
82+
canBan = true,
83+
canKick = true,
84+
myUserRole = RoomMember.Role.ADMIN,
85+
targetRoomMember = null
86+
)
87+
createRoomMemberModerationPresenter(room = room).test {
88+
val initialState = awaitState()
89+
initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser))
90+
skipItems(2)
91+
val updatedState = awaitState()
92+
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
93+
assertThat(updatedState.actions).containsExactly(
94+
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
95+
ModerationActionState(action = ModerationAction.KickUser, isEnabled = true),
96+
ModerationActionState(action = ModerationAction.BanUser, isEnabled = true),
97+
)
98+
}
99+
}
100+
101+
@Test
102+
fun `show actions when canBan=true, canKick=true, userRole=Admin and target is User`() = runTest {
103+
val room = aJoinedRoom(
104+
canBan = true,
105+
canKick = true,
106+
myUserRole = RoomMember.Role.ADMIN,
107+
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.USER.powerLevel)
108+
)
109+
createRoomMemberModerationPresenter(room = room).test {
110+
val initialState = awaitState()
111+
initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser))
112+
skipItems(2)
113+
val updatedState = awaitState()
114+
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
115+
assertThat(updatedState.actions).containsExactly(
116+
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
117+
ModerationActionState(action = ModerationAction.KickUser, isEnabled = true),
118+
ModerationActionState(action = ModerationAction.BanUser, isEnabled = true),
119+
)
120+
}
121+
}
122+
123+
@Test
124+
fun `show actions when canBan=true, canKick=true, userRole=Moderator and target is Admin`() = runTest {
125+
val room = aJoinedRoom(
126+
canBan = true,
127+
canKick = true,
128+
myUserRole = RoomMember.Role.MODERATOR,
129+
targetRoomMember = aRoomMember(userId = A_USER_ID, powerLevel = RoomMember.Role.ADMIN.powerLevel)
130+
)
131+
createRoomMemberModerationPresenter(room = room).test {
132+
val initialState = awaitState()
133+
initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser))
134+
skipItems(2)
135+
val updatedState = awaitState()
136+
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
137+
assertThat(updatedState.actions).containsExactly(
138+
ModerationActionState(action = ModerationAction.DisplayProfile, isEnabled = true),
139+
ModerationActionState(action = ModerationAction.KickUser, isEnabled = false),
140+
ModerationActionState(action = ModerationAction.BanUser, isEnabled = false),
141+
)
142+
}
143+
}
144+
145+
@Test
146+
fun `present - process kick action sets confirming state`() = runTest {
147+
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
148+
val initialState = awaitState()
149+
initialState.eventSink(
150+
RoomMemberModerationEvents.ProcessAction(
151+
targetUser = targetUser,
152+
action = ModerationAction.KickUser
153+
)
154+
)
155+
skipItems(1)
156+
val updatedState = awaitState()
157+
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
158+
assertThat(updatedState.kickUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
159+
}
160+
}
161+
162+
@Test
163+
fun `present - process ban action sets confirming state`() = runTest {
164+
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
165+
val initialState = awaitState()
166+
initialState.eventSink(
167+
RoomMemberModerationEvents.ProcessAction(
168+
targetUser = targetUser,
169+
action = ModerationAction.BanUser
170+
)
171+
)
172+
skipItems(1)
173+
val updatedState = awaitState()
174+
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
175+
assertThat(updatedState.banUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
176+
}
177+
}
178+
179+
@Test
180+
fun `present - process unban action sets confirming state`() = runTest {
181+
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
182+
val initialState = awaitState()
183+
initialState.eventSink(
184+
RoomMemberModerationEvents.ProcessAction(
185+
targetUser = targetUser,
186+
action = ModerationAction.UnbanUser
187+
)
188+
)
189+
skipItems(1)
190+
val updatedState = awaitState()
191+
assertThat(updatedState.selectedUser).isEqualTo(targetUser)
192+
assertThat(updatedState.unbanUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
193+
}
194+
}
195+
196+
@Test
197+
fun `present - do kick user with success`() = runTest {
198+
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
199+
val initialState = awaitState()
200+
initialState.eventSink(
201+
RoomMemberModerationEvents.ProcessAction(
202+
targetUser = targetUser,
203+
action = ModerationAction.KickUser
204+
)
205+
)
206+
skipItems(2)
207+
initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason"))
208+
skipItems(1)
209+
val loadingState = awaitState()
210+
assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
211+
val successState = awaitState()
212+
assertThat(successState.kickUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java)
213+
assertThat(successState.selectedUser).isNull()
214+
}
215+
}
216+
217+
@Test
218+
fun `present - do ban user with success`() = runTest {
219+
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
220+
val initialState = awaitState()
221+
initialState.eventSink(
222+
RoomMemberModerationEvents.ProcessAction(
223+
targetUser = targetUser,
224+
action = ModerationAction.BanUser
225+
)
226+
)
227+
skipItems(2)
228+
initialState.eventSink(InternalRoomMemberModerationEvents.DoBanUser("Reason"))
229+
skipItems(1)
230+
val loadingState = awaitState()
231+
assertThat(loadingState.banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
232+
val successState = awaitState()
233+
assertThat(successState.banUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java)
234+
assertThat(successState.selectedUser).isNull()
235+
}
236+
}
237+
238+
@Test
239+
fun `present - do unban user with success`() = runTest {
240+
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
241+
val initialState = awaitState()
242+
initialState.eventSink(
243+
RoomMemberModerationEvents.ProcessAction(
244+
targetUser = targetUser,
245+
action = ModerationAction.UnbanUser
246+
)
247+
)
248+
skipItems(2)
249+
initialState.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser)
250+
skipItems(1)
251+
val loadingState = awaitState()
252+
assertThat(loadingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
253+
val successState = awaitState()
254+
assertThat(successState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Success::class.java)
255+
assertThat(successState.selectedUser).isNull()
256+
}
257+
}
258+
259+
@Test
260+
fun `present - do kick user with failure`() = runTest {
261+
val error = RuntimeException("Test error")
262+
val room = aJoinedRoom(
263+
kickUserResult = Result.failure(error),
264+
)
265+
createRoomMemberModerationPresenter(room = room).test {
266+
val initialState = awaitState()
267+
initialState.eventSink(
268+
RoomMemberModerationEvents.ProcessAction(
269+
targetUser = targetUser,
270+
action = ModerationAction.KickUser
271+
)
272+
)
273+
skipItems(2)
274+
initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason"))
275+
skipItems(1)
276+
val loadingState = awaitState()
277+
assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
278+
val failureState = awaitState()
279+
assertThat(failureState.kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
280+
}
281+
}
282+
283+
@Test
284+
fun `present - reset clears all async actions and selected user`() = runTest {
285+
createRoomMemberModerationPresenter(room = aJoinedRoom()).test {
286+
val initialState = awaitState()
287+
initialState.eventSink(
288+
RoomMemberModerationEvents.ProcessAction(targetUser = targetUser, action = ModerationAction.BanUser)
289+
)
290+
skipItems(2)
291+
initialState.eventSink(InternalRoomMemberModerationEvents.Reset)
292+
skipItems(1)
293+
val resetState = awaitState()
294+
assertThat(resetState.selectedUser).isNull()
295+
assertThat(resetState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
296+
}
297+
}
298+
299+
private fun aJoinedRoom(
300+
canKick: Boolean = false,
301+
canBan: Boolean = false,
302+
myUserRole: RoomMember.Role = RoomMember.Role.USER,
303+
kickUserResult: Result<Unit> = Result.success(Unit),
304+
banUserResult: Result<Unit> = Result.success(Unit),
305+
unBanUserResult: Result<Unit> = Result.success(Unit),
306+
targetRoomMember: RoomMember? = null,
307+
): JoinedRoom {
308+
return FakeJoinedRoom(
309+
kickUserResult = { _, _ -> kickUserResult },
310+
banUserResult = { _, _ -> banUserResult },
311+
unBanUserResult = { _, _ -> unBanUserResult },
312+
baseRoom = FakeBaseRoom(
313+
canBanResult = { _ -> Result.success(canBan) },
314+
canKickResult = { _ -> Result.success(canKick) },
315+
userRoleResult = { Result.success(myUserRole) },
316+
),
317+
).apply {
318+
val roomMembers = listOfNotNull(targetRoomMember).toPersistentList()
319+
givenRoomMembersState(state = RoomMembersState.Ready(roomMembers))
320+
}
321+
}
322+
323+
private fun TestScope.createRoomMemberModerationPresenter(
324+
room: JoinedRoom,
325+
dispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
326+
analyticsService: AnalyticsService = FakeAnalyticsService(),
327+
): RoomMemberModerationPresenter {
328+
return RoomMemberModerationPresenter(
329+
room = room,
330+
dispatchers = dispatchers,
331+
analyticsService = analyticsService,
332+
)
333+
}
334+
335+
private suspend fun TurbineTestContext<RoomMemberModerationState>.awaitState(): InternalRoomMemberModerationState {
336+
return awaitItem() as InternalRoomMemberModerationState
337+
}
338+
}

0 commit comments

Comments
 (0)