From 198bff99158e8321b9b9a863e7f9390fd6cf6a23 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 22 May 2025 21:41:44 +0200 Subject: [PATCH] fix (left room membership) : use correct membership change and add test --- .../matrix/api/room/RoomMembershipObserver.kt | 17 ++- .../matrix/impl/room/RustBaseRoom.kt | 3 +- .../impl/fixtures/fakes/FakeRustRoom.kt | 5 + .../matrix/impl/room/RustBaseRoomTest.kt | 109 +++++++++++++++++- 4 files changed, 120 insertions(+), 14 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt index 090f196269..19d7fdaaf2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt @@ -22,15 +22,12 @@ class RoomMembershipObserver { private val _updates = MutableSharedFlow(extraBufferCapacity = 10) val updates = _updates.asSharedFlow() - suspend fun notifyUserLeftRoom(roomId: RoomId) { - _updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.LEFT)) - } - - suspend fun notifyUserDeclinedInvite(roomId: RoomId) { - _updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.INVITATION_REJECTED)) - } - - suspend fun notifyUserCanceledKnock(roomId: RoomId) { - _updates.emit(RoomMembershipUpdate(roomId, false, MembershipChange.KNOCK_RETRACTED)) + suspend fun notifyUserLeftRoom(roomId: RoomId, membershipBeforeLeft: CurrentUserMembership) { + val membershipChange = when (membershipBeforeLeft) { + CurrentUserMembership.INVITED -> MembershipChange.INVITATION_REJECTED + CurrentUserMembership.KNOCKED -> MembershipChange.KNOCK_RETRACTED + else -> MembershipChange.LEFT + } + _updates.emit(RoomMembershipUpdate(roomId, false, membershipChange)) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index 10c07a1ac9..3a7a78d444 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -137,10 +137,11 @@ class RustBaseRoom( } override suspend fun leave(): Result = withContext(roomDispatcher) { + val membershipBeforeLeft = roomInfoFlow.value.currentUserMembership runCatching { innerRoom.leave() }.onSuccess { - roomMembershipObserver.notifyUserLeftRoom(roomId) + roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt index 8704f5ee1b..1e1146a038 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt @@ -18,6 +18,7 @@ class FakeRustRoom( private val roomId: RoomId = A_ROOM_ID, private val getMembers: () -> RoomMembersIterator = { lambdaError() }, private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() }, + private val leaveLambda: () -> Unit = { lambdaError() }, ) : Room(NoPointer) { override fun id(): String { return roomId.value @@ -31,6 +32,10 @@ class FakeRustRoom( return getMembersNoSync() } + override suspend fun leave() { + leaveLambda() + } + override fun close() { // No-op } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt index 8a9050a884..990b3c29fa 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt @@ -7,8 +7,12 @@ package io.element.android.libraries.matrix.impl.room +import app.cash.turbine.test 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.timeline.item.event.MembershipChange 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.test.A_DEVICE_ID @@ -16,6 +20,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.isActive import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -33,23 +39,120 @@ class RustBaseRoomTest { assertThat(rustBaseRoom.roomCoroutineScope.isActive).isFalse() } + @Test + fun `when currentUserMembership=JOINED and user leave room succeed then roomMembershipObserver emits change as LEFT`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val rustBaseRoom = createRustBaseRoom( + sessionCoroutineScope = this, + initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.JOINED), + innerRoom = FakeRustRoom( + leaveLambda = { + // Simulate a successful leave + } + ), + roomMembershipObserver = roomMembershipObserver, + ) + val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1) + rustBaseRoom.leave() + shared.test { + val membershipUpdate = awaitItem() + assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isUserInRoom).isFalse() + assertThat(membershipUpdate.change).isEqualTo(MembershipChange.LEFT) + ensureAllEventsConsumed() + } + rustBaseRoom.destroy() + } + + @Test + fun `when currentUserMembership=KNOCKED and user leave room succeed then roomMembershipObserver emits change as KNOCK_RETRACTED`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val rustBaseRoom = createRustBaseRoom( + sessionCoroutineScope = this, + initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.KNOCKED), + innerRoom = FakeRustRoom( + leaveLambda = { + // Simulate a successful leave + } + ), + roomMembershipObserver = roomMembershipObserver, + ) + val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1) + rustBaseRoom.leave() + shared.test { + val membershipUpdate = awaitItem() + assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isUserInRoom).isFalse() + assertThat(membershipUpdate.change).isEqualTo(MembershipChange.KNOCK_RETRACTED) + ensureAllEventsConsumed() + } + rustBaseRoom.destroy() + } + + @Test + fun `when currentUserMembership=INVITED and user leave room succeed then roomMembershipObserver emits change as INVITATION_REJECTED`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val rustBaseRoom = createRustBaseRoom( + sessionCoroutineScope = this, + initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED), + innerRoom = FakeRustRoom( + leaveLambda = { + // Simulate a successful leave + } + ), + roomMembershipObserver = roomMembershipObserver, + ) + val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1) + rustBaseRoom.leave() + shared.test { + val membershipUpdate = awaitItem() + assertThat(membershipUpdate.roomId).isEqualTo(rustBaseRoom.roomId) + assertThat(membershipUpdate.isUserInRoom).isFalse() + assertThat(membershipUpdate.change).isEqualTo(MembershipChange.INVITATION_REJECTED) + ensureAllEventsConsumed() + } + rustBaseRoom.destroy() + } + + @Test + fun `when user leave room fails then roomMembershipObserver emits nothing`() = runTest { + val roomMembershipObserver = RoomMembershipObserver() + val rustBaseRoom = createRustBaseRoom( + sessionCoroutineScope = this, + initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED), + innerRoom = FakeRustRoom( + leaveLambda = { error("Leave failed") } + ), + roomMembershipObserver = roomMembershipObserver, + ) + val shared = roomMembershipObserver.updates.shareIn(scope = backgroundScope, started = SharingStarted.Eagerly, replay = 1) + rustBaseRoom.leave() + shared.test { + ensureAllEventsConsumed() + } + rustBaseRoom.destroy() + } + private fun TestScope.createRustBaseRoom( sessionCoroutineScope: CoroutineScope, + initialRoomInfo: RoomInfo = aRoomInfo(), + innerRoom: FakeRustRoom = FakeRustRoom(), + roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), ): RustBaseRoom { val dispatchers = testCoroutineDispatchers() return RustBaseRoom( sessionId = A_SESSION_ID, deviceId = A_DEVICE_ID, - innerRoom = FakeRustRoom(), + innerRoom = innerRoom, coroutineDispatchers = dispatchers, roomSyncSubscriber = RoomSyncSubscriber( roomListService = FakeRustRoomListService(), dispatchers = dispatchers, ), - roomMembershipObserver = RoomMembershipObserver(), + roomMembershipObserver = roomMembershipObserver, sessionCoroutineScope = sessionCoroutineScope, roomInfoMapper = RoomInfoMapper(), - initialRoomInfo = aRoomInfo(), + initialRoomInfo = initialRoomInfo, ) } }