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 3e2a81daba..e1652381ff 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 @@ -21,6 +21,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() }, private val latestEventLambda: () -> EventTimelineItem? = { lambdaError() }, private val roomInfo: RoomInfo = aRustRoomInfo(id = roomId.value), ) : Room(NoPointer) { @@ -36,6 +37,10 @@ class FakeRustRoom( return getMembersNoSync() } + override suspend fun leave() { + leaveLambda() + } + override suspend fun roomInfo(): RoomInfo { return roomInfo } 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 c05ba349ce..6b2105eb2a 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,14 +7,21 @@ 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 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 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.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.isActive import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -29,22 +36,115 @@ class RustBaseRoomTest { 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.() -> 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() 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, // Not using backgroundScope here, but the test scope sessionCoroutineScope = this, roomInfoMapper = RoomInfoMapper(), - initialRoomInfo = aRoomInfo(), + initialRoomInfo = initialRoomInfo, ) } }