Merge pull request #4765 from element-hq/feature/fga/fix_left_room_membership_change

Fix left room membership change
This commit is contained in:
ganfra 2025-05-27 22:35:32 +02:00 committed by GitHub
commit e847a80aaf
4 changed files with 118 additions and 15 deletions

View file

@ -22,15 +22,12 @@ class RoomMembershipObserver {
private val _updates = MutableSharedFlow<RoomMembershipUpdate>(extraBufferCapacity = 10) private val _updates = MutableSharedFlow<RoomMembershipUpdate>(extraBufferCapacity = 10)
val updates = _updates.asSharedFlow() val updates = _updates.asSharedFlow()
suspend fun notifyUserLeftRoom(roomId: RoomId) { suspend fun notifyUserLeftRoom(roomId: RoomId, membershipBeforeLeft: CurrentUserMembership) {
_updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.LEFT)) val membershipChange = when (membershipBeforeLeft) {
} CurrentUserMembership.INVITED -> MembershipChange.INVITATION_REJECTED
CurrentUserMembership.KNOCKED -> MembershipChange.KNOCK_RETRACTED
suspend fun notifyUserDeclinedInvite(roomId: RoomId) { else -> MembershipChange.LEFT
_updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.INVITATION_REJECTED)) }
} _updates.emit(RoomMembershipUpdate(roomId, false, membershipChange))
suspend fun notifyUserCanceledKnock(roomId: RoomId) {
_updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.KNOCK_RETRACTED))
} }
} }

View file

@ -137,10 +137,11 @@ class RustBaseRoom(
} }
override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) { override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) {
val membershipBeforeLeft = roomInfoFlow.value.currentUserMembership
runCatching { runCatching {
innerRoom.leave() innerRoom.leave()
}.onSuccess { }.onSuccess {
roomMembershipObserver.notifyUserLeftRoom(roomId) roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft)
} }
} }

View file

@ -21,6 +21,7 @@ class FakeRustRoom(
private val roomId: RoomId = A_ROOM_ID, private val roomId: RoomId = A_ROOM_ID,
private val getMembers: () -> RoomMembersIterator = { lambdaError() }, private val getMembers: () -> RoomMembersIterator = { lambdaError() },
private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() }, private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() },
private val leaveLambda: () -> Unit = { lambdaError() },
private val latestEventLambda: () -> EventTimelineItem? = { lambdaError() }, private val latestEventLambda: () -> EventTimelineItem? = { lambdaError() },
private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value), private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value),
) : Room(NoPointer) { ) : Room(NoPointer) {
@ -36,6 +37,10 @@ class FakeRustRoom(
return getMembersNoSync() return getMembersNoSync()
} }
override suspend fun leave() {
leaveLambda()
}
override suspend fun roomInfo(): RoomInfo { override suspend fun roomInfo(): RoomInfo {
return roomInfo return roomInfo
} }

View file

@ -7,14 +7,21 @@
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.room
import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_DEVICE_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -29,22 +36,115 @@ class RustBaseRoomTest {
assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse() assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse()
} }
private fun TestScope.createRustBaseRoom(): RustBaseRoom { @Test
fun `when currentUserMembership=JOINED and user leave room succeed then roomMembershipObserver emits change as LEFT`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()
val rustBaseRoom = createRustBaseRoom(
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.JOINED),
innerRoom = FakeRustRoom(
leaveLambda = {
// Simulate a successful leave
}
),
roomMembershipObserver = roomMembershipObserver,
)
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
val membershipUpdate = awaitItem()
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
assertThat(membershipUpdate.isUserInRoom).isFalse()
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT)
}
}
@Test
fun `when currentUserMembership=KNOCKED and user leave room succeed then roomMembershipObserver emits change as KNOCK_RETRACTED`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()
val rustBaseRoom = createRustBaseRoom(
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.KNOCKED),
innerRoom = FakeRustRoom(
leaveLambda = {
// Simulate a successful leave
}
),
roomMembershipObserver = roomMembershipObserver,
)
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
val membershipUpdate = awaitItem()
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
assertThat(membershipUpdate.isUserInRoom).isFalse()
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED)
}
}
@Test
fun `when currentUserMembership=INVITED and user leave room succeed then roomMembershipObserver emits change as INVITATION_REJECTED`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()
val rustBaseRoom = createRustBaseRoom(
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
innerRoom = FakeRustRoom(
leaveLambda = {
// Simulate a successful leave
}
),
roomMembershipObserver = roomMembershipObserver,
)
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
val membershipUpdate = awaitItem()
assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId)
assertThat(membershipUpdate.isUserInRoom).isFalse()
assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED)
}
}
@Test
fun `when user leave room fails then roomMembershipObserver emits nothing`() = runTest {
val roomMembershipObserver = RoomMembershipObserver()
val rustBaseRoom = createRustBaseRoom(
initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED),
innerRoom = FakeRustRoom(
leaveLambda = { error("Leave failed") }
),
roomMembershipObserver = roomMembershipObserver,
)
leaveRoomAndObserveMembershipChange(roomMembershipObserver, rustBaseRoom) {
// No emit
}
}
private suspend fun TestScope.leaveRoomAndObserveMembershipChange(
roomMembershipObserver: RoomMembershipObserver,
rustBaseRoom: RustBaseRoom,
validate: suspend TurbineTestContext<RoomMembershipObserver.RoomMembershipUpdate>.() -> Unit
) {
val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1)
rustBaseRoom.leave()
shared.test {
validate()
ensureAllEventsConsumed()
}
rustBaseRoom.destroy()
}
private fun TestScope.createRustBaseRoom(
initialRoomInfo: RoomInfo = aRoomInfo(),
innerRoom: FakeRustRoom = FakeRustRoom(),
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
): RustBaseRoom {
val dispatchers = testCoroutineDispatchers() val dispatchers = testCoroutineDispatchers()
return RustBaseRoom( return RustBaseRoom(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
deviceId = A_DEVICE_ID, deviceId = A_DEVICE_ID,
innerRoom = FakeRustRoom(), innerRoom = innerRoom,
coroutineDispatchers = dispatchers, coroutineDispatchers = dispatchers,
roomSyncSubscriber = RoomSyncSubscriber( roomSyncSubscriber = RoomSyncSubscriber(
roomListService = FakeRustRoomListService(), roomListService = FakeRustRoomListService(),
dispatchers = dispatchers, dispatchers = dispatchers,
), ),
roomMembershipObserver = RoomMembershipObserver(), roomMembershipObserver = roomMembershipObserver,
// Not using backgroundScope here, but the test scope // Not using backgroundScope here, but the test scope
sessionCoroutineScope = this, sessionCoroutineScope = this,
roomInfoMapper = RoomInfoMapper(), roomInfoMapper = RoomInfoMapper(),
initialRoomInfo = aRoomInfo(), initialRoomInfo = initialRoomInfo,
) )
} }
} }