From 97c7296217f1213d3434b381c1811d89154f2fbf Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 18 Dec 2023 13:36:54 +0100 Subject: [PATCH] feat: handle reason for conversation.member-leave event [WPB-5877] (#2307) * feat: handle reason for conversation.member-leave event * feat: add a new team member removed system message * detekt * detekt * detekt * fix test * test * even more tests * mote test --- .../com/wire/kalium/logic/data/event/Event.kt | 3 +- .../kalium/logic/data/event/EventMapper.kt | 10 +- .../logic/data/event/MemberLeaveReason.kt | 24 ++ .../logic/data/message/MessageContent.kt | 14 +- .../logic/data/message/MessageMapper.kt | 36 ++- .../data/message/PersistMessageUseCase.kt | 3 +- .../kalium/logic/data/user/UserRepository.kt | 24 +- .../kalium/logic/feature/UserSessionScope.kt | 9 +- .../logic/sync/receiver/TeamEventReceiver.kt | 2 +- .../conversation/MemberLeaveEventHandler.kt | 86 ++++--- .../MLSConversationRepositoryTest.kt | 5 +- .../feature/client/DeleteClientUseCaseTest.kt | 4 +- .../AddMemberToConversationUseCaseTest.kt | 6 +- ...OrCreateOneToOneConversationUseCaseTest.kt | 4 +- .../conversation/mls/OneOnOneMigratorTest.kt | 4 +- .../conversation/mls/OneOnOneResolverTest.kt | 4 +- .../protocol/OneOnOneProtocolSelectorTest.kt | 4 +- .../logic/framework/TestConversation.kt | 5 +- .../wire/kalium/logic/framework/TestEvent.kt | 4 +- .../sync/receiver/UserEventReceiverTest.kt | 17 +- .../MemberLeaveEventHandlerTest.kt | 212 +++++++++++++----- .../arrangement/UserRepositoryArrangement.kt | 142 ------------ .../arrangement/dao/MemberDAOArrangement.kt | 17 +- .../repository/UserRepositoryArrangement.kt | 153 +++++++++++++ .../PersistMessageUseCaseArrangement.kt | 14 +- .../conversation/ConversationEvent.kt | 10 +- .../notification/EventContentDTO.kt | 25 +-- .../notification/MemberLeaveReasonDTO.kt | 41 ++++ .../wire/kalium/model/EventContentDTOJson.kt | 5 +- .../com/wire/kalium/persistence/Users.sq | 8 +- .../wire/kalium/persistence/dao/UserDAO.kt | 2 + .../kalium/persistence/dao/UserDAOImpl.kt | 12 + .../persistence/dao/message/MessageEntity.kt | 14 +- .../persistence/dao/message/MessageMapper.kt | 11 +- .../kalium/persistence/dao/UserDAOTest.kt | 36 ++- 35 files changed, 654 insertions(+), 316 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/MemberLeaveReason.kt delete mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/UserRepositoryArrangement.kt create mode 100644 network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/MemberLeaveReasonDTO.kt diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt index 0c998445a2c..28381e2cd10 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt @@ -180,7 +180,8 @@ sealed class Event(open val id: String, open val transient: Boolean, open val li override val live: Boolean, val removedBy: UserId, val removedList: List, - val timestampIso: String + val timestampIso: String, + val reason: MemberLeaveReason ) : Conversation(id, transient, live, conversationId) { override fun toLogMap(): Map = mapOf( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt index 91a0b0871d3..954fb340a53 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt @@ -41,6 +41,7 @@ import com.wire.kalium.logic.data.user.toModel import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.util.Base64 import com.wire.kalium.network.api.base.authenticated.featureConfigs.FeatureConfigData +import com.wire.kalium.network.api.base.authenticated.notification.MemberLeaveReasonDTO import com.wire.kalium.network.api.base.authenticated.notification.EventContentDTO import com.wire.kalium.network.api.base.authenticated.notification.EventResponse import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi.PropertyKey.WIRE_RECEIPT_MODE @@ -475,10 +476,11 @@ class EventMapper( id = id, conversationId = eventContentDTO.qualifiedConversation.toModel(), removedBy = eventContentDTO.qualifiedFrom.toModel(), - removedList = eventContentDTO.members.qualifiedUserIds.map { it.toModel() }, + removedList = eventContentDTO.removedUsers.qualifiedUserIds.map { it.toModel() }, timestampIso = eventContentDTO.time, transient = transient, live = live, + reason = eventContentDTO.removedUsers.reason.toModel() ) private fun memberUpdate( @@ -702,3 +704,9 @@ class EventMapper( ) } + +private fun MemberLeaveReasonDTO.toModel(): MemberLeaveReason = when (this) { + MemberLeaveReasonDTO.LEFT -> MemberLeaveReason.Left + MemberLeaveReasonDTO.REMOVED -> MemberLeaveReason.Removed + MemberLeaveReasonDTO.USER_DELETED -> MemberLeaveReason.UserDeleted + } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/MemberLeaveReason.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/MemberLeaveReason.kt new file mode 100644 index 00000000000..dbe3f30eabf --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/MemberLeaveReason.kt @@ -0,0 +1,24 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.event + +sealed interface MemberLeaveReason { + object Left : MemberLeaveReason + object Removed : MemberLeaveReason + object UserDeleted : MemberLeaveReason +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt index c64411b3e0f..2e9b7d61918 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt @@ -230,6 +230,7 @@ sealed class MessageContent { sealed class MemberChange(open val members: List) : System() { data class Added(override val members: List) : MemberChange(members) data class Removed(override val members: List) : MemberChange(members) + data class RemovedFromTeam(override val members: List) : MemberChange(members) data class FailedToAdd(override val members: List) : MemberChange(members) data class CreationAdded(override val members: List) : MemberChange(members) data class FederationRemoved(override val members: List) : MemberChange(members) @@ -243,6 +244,7 @@ sealed class MessageContent { data class ConversationRenamed(val conversationName: String) : System() + @Deprecated("Use MemberChange.RemovedFromTeam instead") data class TeamMemberRemoved(val userName: String) : System() data object MissedCall : System() @@ -356,9 +358,9 @@ fun MessageContent?.getType() = when (this) { is MessageContent.HistoryLostProtocolChanged -> "HistoryLostProtocolChanged" is MessageContent.MemberChange.Added -> "MemberChange.Added" is MessageContent.MemberChange.Removed -> "MemberChange.Removed" + is MessageContent.MemberChange.RemovedFromTeam -> "MemberChange.RemovedFromTeam" is MessageContent.MissedCall -> "MissedCall" is MessageContent.NewConversationReceiptMode -> "NewConversationReceiptMode" - is MessageContent.TeamMemberRemoved -> "TeamMemberRemoved" is MessageContent.ConversationCreated -> "ConversationCreated" is MessageContent.MemberChange.CreationAdded -> "MemberChange.CreationAdded" is MessageContent.MemberChange.FailedToAdd -> "MemberChange.FailedToAdd" @@ -377,6 +379,7 @@ fun MessageContent?.getType() = when (this) { MessageContent.ConversationVerifiedProteus -> "ConversationVerification.Verified.Proteus" is MessageContent.ConversationStartedUnverifiedWarning -> "ConversationStartedUnverifiedWarning" is MessageContent.Location -> "Location" + is MessageContent.TeamMemberRemoved -> "TeamMemberRemoved" is MessageContent.LegalHold.ForConversation.Disabled -> "LegalHold.ForConversation.Disabled" is MessageContent.LegalHold.ForConversation.Enabled -> "LegalHold.ForConversation.Enabled" is MessageContent.LegalHold.ForMembers.Disabled -> "LegalHold.ForMembers.Disabled" @@ -412,7 +415,13 @@ sealed interface MessagePreviewContent { val otherUserIdList: List // TODO add usernames ) : WithUser - data class MembersRemoved( + data class ConversationMembersRemoved( + override val username: String?, + val isSelfUserRemoved: Boolean, + val otherUserIdList: List // TODO add usernames + ) : WithUser + + data class TeamMembersRemoved( override val username: String?, val isSelfUserRemoved: Boolean, val otherUserIdList: List // TODO add usernames @@ -432,6 +441,7 @@ sealed interface MessagePreviewContent { data class ConversationNameChange(override val username: String?) : WithUser + @Deprecated("Use WithUser.TeamMembersRemoved instead") data class TeamMemberRemoved(override val username: String?) : WithUser data class MissedCall(override val username: String?) : WithUser diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt index 3a8c51fbc68..f93801e1fbc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt @@ -270,6 +270,7 @@ class MessageMapperImpl( message.date, LocalNotificationCommentType.LOCATION ) + MessageEntity.ContentType.MEMBER_CHANGE -> null MessageEntity.ContentType.RESTRICTED_ASSET -> null MessageEntity.ContentType.CONVERSATION_RENAMED -> null @@ -383,17 +384,18 @@ class MessageMapperImpl( } @Suppress("ComplexMethod") -fun MessageEntityContent.System.toMessageContent(): MessageContent.System = when (this) { - is MessageEntityContent.MemberChange -> { - val memberList = this.memberUserIdList.map { it.toModel() } - when (this.memberChangeType) { - MessageEntity.MemberChangeType.ADDED -> MessageContent.MemberChange.Added(memberList) - MessageEntity.MemberChangeType.REMOVED -> MessageContent.MemberChange.Removed(memberList) - MessageEntity.MemberChangeType.CREATION_ADDED -> MessageContent.MemberChange.CreationAdded(memberList) - MessageEntity.MemberChangeType.FAILED_TO_ADD -> MessageContent.MemberChange.FailedToAdd(memberList) - MessageEntity.MemberChangeType.FEDERATION_REMOVED -> MessageContent.MemberChange.FederationRemoved(memberList) + fun MessageEntityContent.System.toMessageContent(): MessageContent.System = when (this) { + is MessageEntityContent.MemberChange -> { + val memberList = this.memberUserIdList.map { it.toModel() } + when (this.memberChangeType) { + MessageEntity.MemberChangeType.ADDED -> MessageContent.MemberChange.Added(memberList) + MessageEntity.MemberChangeType.REMOVED -> MessageContent.MemberChange.Removed(memberList) + MessageEntity.MemberChangeType.CREATION_ADDED -> MessageContent.MemberChange.CreationAdded(memberList) + MessageEntity.MemberChangeType.FAILED_TO_ADD -> MessageContent.MemberChange.FailedToAdd(memberList) + MessageEntity.MemberChangeType.FEDERATION_REMOVED -> MessageContent.MemberChange.FederationRemoved(memberList) + MessageEntity.MemberChangeType.REMOVED_FROM_TEAM -> MessageContent.MemberChange.RemovedFromTeam(memberList) + } } - } is MessageEntityContent.MissedCall -> MessageContent.MissedCall is MessageEntityContent.ConversationRenamed -> MessageContent.ConversationRenamed(conversationName) @@ -454,7 +456,7 @@ private fun MessagePreviewEntityContent.toMessageContent(): MessagePreviewConten otherUserIdList = otherUserIdList.map { it.toModel() } ) - is MessagePreviewEntityContent.MembersRemoved -> MessagePreviewContent.WithUser.MembersRemoved( + is MessagePreviewEntityContent.ConversationMembersRemoved -> MessagePreviewContent.WithUser.ConversationMembersRemoved( username = senderName, isSelfUserRemoved = isContainSelfUserId, otherUserIdList = otherUserIdList.map { it.toModel() } @@ -477,11 +479,17 @@ private fun MessagePreviewEntityContent.toMessageContent(): MessagePreviewConten otherUserIdList = otherUserIdList.map { it.toModel() } ) + is MessagePreviewEntityContent.TeamMembersRemoved -> MessagePreviewContent.WithUser.TeamMembersRemoved( + username = senderName, + isSelfUserRemoved = isContainSelfUserId, + otherUserIdList = otherUserIdList.map { it.toModel() } + ) + is MessagePreviewEntityContent.Ephemeral -> MessagePreviewContent.Ephemeral(isGroupConversation) is MessagePreviewEntityContent.MentionedSelf -> MessagePreviewContent.WithUser.MentionedSelf(senderName) is MessagePreviewEntityContent.MissedCall -> MessagePreviewContent.WithUser.MissedCall(senderName) is MessagePreviewEntityContent.QuotedSelf -> MessagePreviewContent.WithUser.QuotedSelf(senderName) - is MessagePreviewEntityContent.TeamMemberRemoved -> MessagePreviewContent.WithUser.TeamMemberRemoved(userName) + is MessagePreviewEntityContent.TeamMemberRemoved_Legacy -> MessagePreviewContent.WithUser.TeamMemberRemoved(userName) is MessagePreviewEntityContent.Text -> MessagePreviewContent.WithUser.Text(username = senderName, messageBody = messageBody) is MessagePreviewEntityContent.CryptoSessionReset -> MessagePreviewContent.CryptoSessionReset MessagePreviewEntityContent.Unknown -> MessagePreviewContent.Unknown @@ -623,6 +631,10 @@ fun MessageContent.System.toMessageEntityContent(): MessageEntityContent.System MessageEntity.MemberChangeType.FEDERATION_REMOVED ) + is MessageContent.MemberChange.RemovedFromTeam -> MessageEntityContent.MemberChange( + memberUserIdList, + MessageEntity.MemberChangeType.REMOVED_FROM_TEAM + ) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt index a481f35a18c..11bf83a7c96 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt @@ -85,7 +85,6 @@ internal class PersistMessageUseCaseImpl( is MessageContent.Reaction -> false is MessageContent.Cleared -> false is MessageContent.ConversationRenamed -> true - is MessageContent.TeamMemberRemoved -> false is MessageContent.Receipt -> false is MessageContent.ClientAction -> false is MessageContent.CryptoSessionReset -> false @@ -112,5 +111,7 @@ internal class PersistMessageUseCaseImpl( is MessageContent.ConversationStartedUnverifiedWarning -> false is MessageContent.Location -> true is MessageContent.LegalHold -> false + is MessageContent.MemberChange.RemovedFromTeam -> false + is MessageContent.TeamMemberRemoved -> false } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt index 4558325f7cb..8084a625234 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.data.id.IdMapper import com.wire.kalium.logic.data.id.NetworkQualifiedId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.id.toModel @@ -112,6 +113,8 @@ interface UserRepository { suspend fun updateUserFromEvent(event: Event.User.Update): Either suspend fun markUserAsDeletedAndRemoveFromGroupConversations(userId: UserId): Either + suspend fun markUserAsDeletedAndRemoveFromGroupConversations(userId: List): Either + /** * Marks federated user as defederated in order to hold conversation history * when backends stops federating. @@ -140,6 +143,8 @@ interface UserRepository { suspend fun updateSupportedProtocols(protocols: Set): Either suspend fun updateActiveOneOnOneConversation(userId: UserId, conversationId: ConversationId): Either + + suspend fun isAtLeastOneUserATeamMember(userId: List, teamId: TeamId): Either } @Suppress("LongParameterList", "TooManyFunctions") @@ -316,8 +321,8 @@ internal class UserDataSource internal constructor( userProfile = userProfileDTO, connectionState = ConnectionEntity.State.ACCEPTED, userTypeEntity = - if (userProfileDTO.service != null) UserTypeEntity.SERVICE - else userTypeEntityMapper.teamRoleCodeToUserType(mapTeamMemberDTO[userProfileDTO.id.value]?.permissions?.own) + if (userProfileDTO.service != null) UserTypeEntity.SERVICE + else userTypeEntityMapper.teamRoleCodeToUserType(mapTeamMemberDTO[userProfileDTO.id.value]?.permissions?.own) ) } val otherUsers = listUserProfileDTO @@ -461,6 +466,10 @@ internal class UserDataSource internal constructor( override suspend fun updateActiveOneOnOneConversation(userId: UserId, conversationId: ConversationId): Either = wrapStorageRequest { userDAO.updateActiveOneOnOneConversation(userId.toDao(), conversationId.toDao()) } + override suspend fun isAtLeastOneUserATeamMember(userId: List, teamId: TeamId) = wrapStorageRequest { + userDAO.isAtLeastOneUserATeamMember(userId.map { it.toDao() }, teamId.value) + } + override fun observeAllKnownUsersNotInConversation( conversationId: ConversationId ): Flow>> { @@ -502,12 +511,15 @@ internal class UserDataSource internal constructor( } } - override suspend fun markUserAsDeletedAndRemoveFromGroupConversations(userId: UserId): Either { - return wrapStorageRequest { - userDAO.markUserAsDeletedAndRemoveFromGroupConv(userId.toDao()) - } + override suspend fun markUserAsDeletedAndRemoveFromGroupConversations(userId: UserId): Either = wrapStorageRequest { + userDAO.markUserAsDeletedAndRemoveFromGroupConv(userId.toDao()) } + override suspend fun markUserAsDeletedAndRemoveFromGroupConversations(userId: List): Either = + wrapStorageRequest { + userDAO.markUserAsDeletedAndRemoveFromGroupConv(userId.map { it.toDao() }) + } + override suspend fun defederateUser(userId: UserId): Either { return wrapStorageRequest { userDAO.markUserAsDefederated(userId.toDao()) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index 1a474891ecb..c5488558a46 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -1164,9 +1164,10 @@ class UserSessionScope internal constructor( federatedIdMapper = federatedIdMapper ) - private val updateConversationClientsForCurrentCall: Lazy = lazy { - UpdateConversationClientsForCurrentCallUseCaseImpl(callRepository, conversationClientsInCallUpdater) - } + private val updateConversationClientsForCurrentCall: Lazy + get() = lazy { + UpdateConversationClientsForCurrentCallUseCaseImpl(callRepository, conversationClientsInCallUpdater) + } private val reactionRepository = ReactionRepositoryImpl(userId, userStorage.database.reactionDAO) private val receiptRepository = ReceiptRepositoryImpl(userStorage.database.receiptDAO) @@ -1276,7 +1277,7 @@ class UserSessionScope internal constructor( ) private val memberLeaveHandler: MemberLeaveEventHandler get() = MemberLeaveEventHandlerImpl( - userStorage.database.memberDAO, userRepository, persistMessage, updateConversationClientsForCurrentCall + userStorage.database.memberDAO, userRepository, persistMessage, updateConversationClientsForCurrentCall, selfTeamId ) private val memberChangeHandler: MemberChangeEventHandler get() = MemberChangeEventHandlerImpl( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/TeamEventReceiver.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/TeamEventReceiver.kt index b0d164488c2..36146b1eb35 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/TeamEventReceiver.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/TeamEventReceiver.kt @@ -43,7 +43,7 @@ internal class TeamEventReceiverImpl( } // TODO: Make sure errors are accounted for by each handler. // onEvent now requires Either, so we can propagate errors, - // but not all handlers are using it yet.® + // but not all handlers are using it yet. // Returning Either.Right is the equivalent of how it was originally working. return Either.Right(Unit) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt index d5cbd7614a8..7911960110c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt @@ -21,8 +21,10 @@ package com.wire.kalium.logic.sync.receiver.conversation import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.event.EventLoggingStatus +import com.wire.kalium.logic.data.event.MemberLeaveReason import com.wire.kalium.logic.data.event.logEventProcessing import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.SelfTeamIdProvider import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent @@ -31,7 +33,8 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCase import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.getOrElse +import com.wire.kalium.logic.functional.getOrNull import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.logic.functional.onSuccess import com.wire.kalium.logic.kaliumLogger @@ -47,42 +50,75 @@ internal class MemberLeaveEventHandlerImpl( private val userRepository: UserRepository, private val persistMessage: PersistMessageUseCase, private val updateConversationClientsForCurrentCall: Lazy, + private val selfTeamIdProvider: SelfTeamIdProvider ) : MemberLeaveEventHandler { override suspend fun handle(event: Event.Conversation.MemberLeave) = - deleteMembers(event.removedList, event.conversationId) - .flatMap { - // fetch required unknown users that haven't been persisted during slow sync, e.g. from another team - // and keep them to properly show this member-leave message - userRepository.fetchUsersIfUnknownByIds(event.removedList.toSet()) + let { + when (event.reason) { + MemberLeaveReason.Removed, + MemberLeaveReason.Left -> { + deleteMembers(event.removedList, event.conversationId) + } + + MemberLeaveReason.UserDeleted -> { + userRepository.markUserAsDeletedAndRemoveFromGroupConversations(event.removedList) + } } - .onSuccess { - updateConversationClientsForCurrentCall.value(event.conversationId) - val message = Message.System( + }.onSuccess { + updateConversationClientsForCurrentCall.value(event.conversationId) + }.onSuccess { + // fetch required unknown users that haven't been persisted during slow sync, e.g. from another team + // and keep them to properly show this member-leave message + userRepository.fetchUsersIfUnknownByIds(event.removedList.toSet()) + }.onSuccess { + val content: MessageContent.System? = resolveMessageContent(event) + + content?.let { + Message.System( id = event.id, - content = MessageContent.MemberChange.Removed(members = event.removedList), + content = it, conversationId = event.conversationId, date = event.timestampIso, senderUserId = event.removedBy, status = Message.Status.Sent, visibility = Message.Visibility.VISIBLE, expirationData = null - ) - persistMessage(message) - kaliumLogger - .logEventProcessing( - EventLoggingStatus.SUCCESS, - event - ) - } - .onFailure { - kaliumLogger - .logEventProcessing( - EventLoggingStatus.FAILURE, - event, - Pair("errorInfo", "$it") - ) + ).also { + persistMessage(it) + } } + }.onSuccess { + kaliumLogger + .logEventProcessing( + EventLoggingStatus.SUCCESS, + event + ) + }.onFailure { + kaliumLogger + .logEventProcessing( + EventLoggingStatus.FAILURE, + event, + Pair("errorInfo", "$it") + ) + } + + private suspend fun resolveMessageContent(event: Event.Conversation.MemberLeave): MessageContent.System? { + return when (event.reason) { + MemberLeaveReason.Left, + MemberLeaveReason.Removed -> MessageContent.MemberChange.Removed(members = event.removedList) + MemberLeaveReason.UserDeleted -> handleUserDeleted(event) + } + } + private suspend fun handleUserDeleted(event: Event.Conversation.MemberLeave): MessageContent.System? { + val teamId = selfTeamIdProvider().getOrNull() ?: return null + val isMemberRemoved = userRepository.isAtLeastOneUserATeamMember(event.removedList, teamId).getOrElse(false) + return if (isMemberRemoved) { + MessageContent.MemberChange.RemovedFromTeam(members = event.removedList) + } else { + null + } + } private suspend fun deleteMembers( userIDList: List, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt index 54ad963f9ed..d6538338fe4 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt @@ -55,12 +55,13 @@ import com.wire.kalium.logic.util.shouldSucceed import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.client.DeviceTypeDTO import com.wire.kalium.network.api.base.authenticated.client.SimpleClientResponse +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMemberRemovedDTO import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMembers -import com.wire.kalium.network.api.base.authenticated.conversation.ConversationUsers import com.wire.kalium.network.api.base.authenticated.keypackage.KeyPackageDTO import com.wire.kalium.network.api.base.authenticated.message.MLSMessageApi import com.wire.kalium.network.api.base.authenticated.message.SendMLSMessageResponse import com.wire.kalium.network.api.base.authenticated.notification.EventContentDTO +import com.wire.kalium.network.api.base.authenticated.notification.MemberLeaveReasonDTO import com.wire.kalium.network.api.base.model.ErrorResponse import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.utils.NetworkResponse @@ -1577,7 +1578,7 @@ class MLSConversationRepositoryTest { TestConversation.NETWORK_ID, TestConversation.NETWORK_USER_ID1, "2022-03-30T15:36:00.000Z", - ConversationUsers(emptyList(), emptyList()), + ConversationMemberRemovedDTO(emptyList(), MemberLeaveReasonDTO.LEFT), TestConversation.NETWORK_USER_ID1.value ) val WELCOME_EVENT = Event.Conversation.MLSWelcome( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/DeleteClientUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/DeleteClientUseCaseTest.kt index c90409ba8a9..0a667a16729 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/DeleteClientUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/DeleteClientUseCaseTest.kt @@ -26,10 +26,10 @@ import com.wire.kalium.logic.feature.user.UpdateSupportedProtocolsAndResolveOneO import com.wire.kalium.logic.framework.TestClient import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.test_util.TestNetworkException -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangement -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.mls.OneOnOneResolverArrangement import com.wire.kalium.logic.util.arrangement.mls.OneOnOneResolverArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl import com.wire.kalium.network.exceptions.KaliumException import io.ktor.utils.io.errors.IOException import io.mockative.Mock diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/AddMemberToConversationUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/AddMemberToConversationUseCaseTest.kt index 90969d8011b..d808572cd5a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/AddMemberToConversationUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/AddMemberToConversationUseCaseTest.kt @@ -24,8 +24,8 @@ import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.conversation.ConversationGroupRepository import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangement -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl import com.wire.kalium.network.api.base.model.ErrorResponse import com.wire.kalium.network.exceptions.KaliumException import io.mockative.Mock @@ -36,12 +36,10 @@ import io.mockative.given import io.mockative.mock import io.mockative.once import io.mockative.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertIs -@OptIn(ExperimentalCoroutinesApi::class) class AddMemberToConversationUseCaseTest { @Test diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/GetOrCreateOneToOneConversationUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/GetOrCreateOneToOneConversationUseCaseTest.kt index 4015859c8b3..91c2873e8b3 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/GetOrCreateOneToOneConversationUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/GetOrCreateOneToOneConversationUseCaseTest.kt @@ -23,12 +23,12 @@ import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangement -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.mls.OneOnOneResolverArrangement import com.wire.kalium.logic.util.arrangement.mls.OneOnOneResolverArrangementImpl import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangement import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl import io.mockative.anything import io.mockative.eq import io.mockative.once diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt index 3585bdaeff9..4feb4a48cca 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt @@ -32,8 +32,8 @@ import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryA import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.repository.MessageRepositoryArrangement import com.wire.kalium.logic.util.arrangement.repository.MessageRepositoryArrangementImpl -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangement -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed import io.mockative.any diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolverTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolverTest.kt index 7b0ddf225ab..266a993047f 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolverTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneResolverTest.kt @@ -24,12 +24,12 @@ import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.util.arrangement.IncrementalSyncRepositoryArrangement import com.wire.kalium.logic.util.arrangement.IncrementalSyncRepositoryArrangementImpl -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangement -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.mls.OneOnOneMigratorArrangement import com.wire.kalium.logic.util.arrangement.mls.OneOnOneMigratorArrangementImpl import com.wire.kalium.logic.util.arrangement.protocol.OneOnOneProtocolSelectorArrangement import com.wire.kalium.logic.util.arrangement.protocol.OneOnOneProtocolSelectorArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed import io.mockative.eq diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelectorTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelectorTest.kt index b468208d3e7..703e1583688 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelectorTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/protocol/OneOnOneProtocolSelectorTest.kt @@ -22,8 +22,8 @@ import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangement -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed import io.mockative.eq diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt index 41d1363fbdd..a69f425ccde 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt @@ -30,14 +30,15 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.network.api.base.authenticated.conversation.ConvProtocol import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMemberAddedResponse import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMemberDTO +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMemberRemovedDTO import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMemberRemovedResponse import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMembers import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMembersResponse import com.wire.kalium.network.api.base.authenticated.conversation.ConversationResponse -import com.wire.kalium.network.api.base.authenticated.conversation.ConversationUsers import com.wire.kalium.network.api.base.authenticated.conversation.ReceiptMode import com.wire.kalium.network.api.base.authenticated.conversation.model.ConversationCodeInfo import com.wire.kalium.network.api.base.authenticated.notification.EventContentDTO +import com.wire.kalium.network.api.base.authenticated.notification.MemberLeaveReasonDTO import com.wire.kalium.network.api.base.model.ConversationAccessDTO import com.wire.kalium.network.api.base.model.ConversationAccessRoleDTO import com.wire.kalium.network.api.base.model.QualifiedID @@ -264,7 +265,7 @@ object TestConversation { NETWORK_ID, NETWORK_USER_ID1, "2022-03-30T15:36:00.000Z", - ConversationUsers(emptyList(), emptyList()), + ConversationMemberRemovedDTO(emptyList(), MemberLeaveReasonDTO.LEFT), NETWORK_USER_ID1.value ) ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt index 3b697dc360d..19f8acab831 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.Conversation.Member import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.event.Event +import com.wire.kalium.logic.data.event.MemberLeaveReason import com.wire.kalium.logic.data.featureConfig.AppLockModel import com.wire.kalium.logic.data.featureConfig.Status import com.wire.kalium.logic.data.user.Connection @@ -52,7 +53,8 @@ object TestEvent { false, TestUser.USER_ID, listOf(), - "2022-03-30T15:36:00.000Z" + "2022-03-30T15:36:00.000Z", + reason = MemberLeaveReason.Left ) fun memberChange(eventId: String = "eventId", member: Member) = Event.Conversation.MemberChanged.MemberChangedRole( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserEventReceiverTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserEventReceiverTest.kt index 014c8b970cc..1530f988b62 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserEventReceiverTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserEventReceiverTest.kt @@ -25,10 +25,10 @@ import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.NewGroupConversationSystemMessagesCreator +import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.auth.LogoutUseCase import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestEvent @@ -36,10 +36,11 @@ import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.sync.receiver.handler.legalhold.LegalHoldHandler import com.wire.kalium.logic.sync.receiver.handler.legalhold.LegalHoldRequestHandler import com.wire.kalium.logic.test_util.TestKaliumDispatcher -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangement -import com.wire.kalium.logic.util.arrangement.UserRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.mls.OneOnOneResolverArrangement import com.wire.kalium.logic.util.arrangement.mls.OneOnOneResolverArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl +import io.mockative.KFunction1 import io.mockative.Mock import io.mockative.any import io.mockative.classOf @@ -109,14 +110,19 @@ class UserEventReceiverTest { fun givenUserDeleteEvent_RepoAndPersisMessageAreInvoked() = runTest { val event = TestEvent.userDelete(userId = OTHER_USER_ID) val (arrangement, eventReceiver) = arrange { - withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess() + withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess( + userIdMatcher = any() + ) withConversationsByUserId(listOf(TestConversation.CONVERSATION)) } eventReceiver.onEvent(event) verify(arrangement.userRepository) - .suspendFunction(arrangement.userRepository::markUserAsDeletedAndRemoveFromGroupConversations) + .suspendFunction( + arrangement.userRepository::markUserAsDeletedAndRemoveFromGroupConversations, + KFunction1() + ) .with(any()) .wasInvoked(exactly = once) } @@ -270,6 +276,7 @@ class UserEventReceiverTest { @Mock val legalHoldRequestHandler = mock(classOf()) + @Mock val legalHoldHandler = mock(classOf()) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandlerTest.kt index 58682933b92..163efcf2764 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandlerTest.kt @@ -19,101 +19,176 @@ package com.wire.kalium.logic.sync.receiver.conversation import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.event.Event +import com.wire.kalium.logic.data.event.MemberLeaveReason import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent -import com.wire.kalium.logic.data.message.PersistMessageUseCase import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCase import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangement +import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangementImpl +import com.wire.kalium.logic.util.arrangement.provider.SelfTeamIdProviderArrangement +import com.wire.kalium.logic.util.arrangement.provider.SelfTeamIdProviderArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl +import com.wire.kalium.logic.util.arrangement.usecase.PersistMessageUseCaseArrangement +import com.wire.kalium.logic.util.arrangement.usecase.PersistMessageUseCaseArrangementImpl import com.wire.kalium.persistence.dao.QualifiedIDEntity -import com.wire.kalium.persistence.dao.member.MemberDAO import io.mockative.Mock +import io.mockative.any import io.mockative.classOf -import io.mockative.given +import io.mockative.eq import io.mockative.mock import io.mockative.once import io.mockative.verify import kotlinx.coroutines.test.runTest -import kotlin.test.BeforeTest import kotlin.test.Test class MemberLeaveEventHandlerTest { - @Mock - private val memberDao = mock(classOf()) + @Test + fun givenDaoReturnsSuccess_whenDeletingMember_thenPersistSystemMessage() = runTest { - @Mock - private val userRepository = mock(classOf()) + val event = memberLeaveEvent(reason = MemberLeaveReason.Left) + val message = memberRemovedMessage(event) - @Mock - private val persistMessage = mock(classOf()) + val (arrangement, memberLeaveEventHandler) = Arrangement() + .arrange { + withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess(any>()) + withFetchUsersIfUnknownByIdsReturning(Either.Right(Unit), userIdList = eq(event.removedList.toSet())) + withPersistingMessage(Either.Right(Unit), messageMatcher = eq(message)) + withDeleteMembersByQualifiedID(conversationId = eq(event.conversationId.toDao()), memberIdList = eq(list)) + } - @Mock - private val updateConversationClientsForCurrentCall = mock(classOf()) + memberLeaveEventHandler.handle(memberLeaveEvent(reason = MemberLeaveReason.Left)) - private lateinit var memberLeaveEventHandler: MemberLeaveEventHandler + verify(arrangement.memberDAO).coroutine { + deleteMembersByQualifiedID(list, qualifiedConversationIdEntity) + }.wasInvoked(once) + + verify(arrangement.updateConversationClientsForCurrentCall).coroutine { + invoke(message.conversationId) + }.wasInvoked(once) + + verify(arrangement.persistMessageUseCase).coroutine { + invoke(message) + }.wasInvoked(once) - @BeforeTest - fun setup() { - memberLeaveEventHandler = MemberLeaveEventHandlerImpl( - memberDAO = memberDao, - userRepository = userRepository, - persistMessage = persistMessage, - updateConversationClientsForCurrentCall = lazy { updateConversationClientsForCurrentCall } - ) } @Test - fun givenDaoReturnsSuccess_whenDeletingMember_thenPersistSystemMessage() = runTest { - given(memberDao).coroutine { + fun givenDaoReturnsFailure_whenDeletingMember_thenNothingToDo() = runTest { + val event = memberLeaveEvent(reason = MemberLeaveReason.Left) + + val (arrangement, memberLeaveEventHandler) = Arrangement() + .arrange { + withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess(any>()) + withFetchUsersIfUnknownByIdsReturning( + Either.Left(failure), + userIdList = eq(event.removedList.toSet()) + ) + withPersistingMessage(Either.Left(failure)) + withDeleteMembersByQualifiedID(throws = IllegalArgumentException()) + } + + memberLeaveEventHandler.handle(memberLeaveEvent(reason = MemberLeaveReason.Left)) + + verify(arrangement.memberDAO).coroutine { deleteMembersByQualifiedID(list, qualifiedConversationIdEntity) - }.then { Either.Right(Unit) } - - given(userRepository).coroutine { - fetchUsersIfUnknownByIds(memberLeaveEvent.removedList.toSet()) - }.then { Either.Right(Unit) } - - given(persistMessage).coroutine { - persistMessage(message) - }.then { Either.Right(Unit) } + }.wasInvoked(once) - memberLeaveEventHandler.handle(memberLeaveEvent) + verify(arrangement.persistMessageUseCase).coroutine { + invoke(memberRemovedMessage(event)) + }.wasNotInvoked() + } - verify(memberDao).coroutine { - deleteMembersByQualifiedID(list, qualifiedConversationIdEntity) + @Test + fun givenDaoReturnsSuccess_whenDeletingMember_thenPersistSystemMessageAndFetchUsers() = runTest { + val event = memberLeaveEvent(reason = MemberLeaveReason.UserDeleted) + val message = memberRemovedFromTeamMessage(event) + + val (arrangement, memberLeaveEventHandler) = Arrangement() + .arrange { + withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess(any>()) + withFetchUsersIfUnknownByIdsReturning(Either.Right(Unit), userIdList = eq(event.removedList.toSet())) + withPersistingMessage(Either.Right(Unit), messageMatcher = eq(message)) + withTeamId(Either.Right(TeamId("teamId"))) + withIsAtLeastOneUserATeamMember(Either.Right(true)) + } + + memberLeaveEventHandler.handle(memberLeaveEvent(reason = MemberLeaveReason.UserDeleted)) + + verify(arrangement.userRepository).coroutine { + markUserAsDeletedAndRemoveFromGroupConversations(event.removedList) }.wasInvoked(once) - verify(updateConversationClientsForCurrentCall).coroutine { - updateConversationClientsForCurrentCall.invoke(message.conversationId) + verify(arrangement.userRepository).coroutine { + fetchUsersIfUnknownByIds(event.removedList.toSet()) }.wasInvoked(once) - verify(persistMessage).coroutine { - persistMessage.invoke(message) + verify(arrangement.updateConversationClientsForCurrentCall).coroutine { + invoke(message.conversationId) }.wasInvoked(once) + verify(arrangement.persistMessageUseCase).coroutine { + invoke(message) + }.wasInvoked(once) } @Test - fun givenDaoReturnsFailure_whenDeletingMember_thenNothingToDo() = runTest { - given(memberDao).coroutine { - deleteMembersByQualifiedID(list, qualifiedConversationIdEntity) - }.then { Either.Left(failure) } + fun givenDaoReturnsSuccess_whenDeletingMemberAndSelfIsNotTeamMember_thenDoNothing() = runTest { + val event = memberLeaveEvent(reason = MemberLeaveReason.UserDeleted) + val (arrangement, memberLeaveEventHandler) = Arrangement() + .arrange { + withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess(any>()) + withFetchUsersIfUnknownByIdsReturning(Either.Right(Unit), userIdList = eq(event.removedList.toSet())) + withTeamId(Either.Right(null)) + } + + memberLeaveEventHandler.handle(memberLeaveEvent(reason = MemberLeaveReason.UserDeleted)) + verify(arrangement.userRepository).coroutine { + markUserAsDeletedAndRemoveFromGroupConversations(event.removedList) + }.wasInvoked(once) - given(userRepository).coroutine { - fetchUsersIfUnknownByIds(memberLeaveEvent.removedList.toSet()) - }.then { Either.Left(failure) } + verify(arrangement.userRepository).coroutine { + fetchUsersIfUnknownByIds(event.removedList.toSet()) + }.wasInvoked(once) - memberLeaveEventHandler.handle(memberLeaveEvent) + verify(arrangement.updateConversationClientsForCurrentCall) + .suspendFunction(arrangement.updateConversationClientsForCurrentCall::invoke) + .with(eq(event.conversationId)) + .wasInvoked(exactly = once) - verify(memberDao).coroutine { - deleteMembersByQualifiedID(list, qualifiedConversationIdEntity) - }.wasInvoked(once) + verify(arrangement.persistMessageUseCase) + .suspendFunction(arrangement.persistMessageUseCase::invoke) + .with(eq(memberRemovedFromTeamMessage(event))) + .wasNotInvoked() + } - verify(persistMessage).coroutine { - persistMessage.invoke(message) - }.wasNotInvoked() + private class Arrangement : UserRepositoryArrangement by UserRepositoryArrangementImpl(), + PersistMessageUseCaseArrangement by PersistMessageUseCaseArrangementImpl(), + MemberDAOArrangement by MemberDAOArrangementImpl(), + SelfTeamIdProviderArrangement by SelfTeamIdProviderArrangementImpl() { + + @Mock + val updateConversationClientsForCurrentCall = mock(classOf()) + + private lateinit var memberLeaveEventHandler: MemberLeaveEventHandler + + fun arrange(block: Arrangement.() -> Unit): Pair = apply(block) + .let { + memberLeaveEventHandler = MemberLeaveEventHandlerImpl( + memberDAO = memberDAO, + userRepository = userRepository, + persistMessage = persistMessageUseCase, + updateConversationClientsForCurrentCall = lazy { updateConversationClientsForCurrentCall }, + selfTeamIdProvider + ) + this to memberLeaveEventHandler + } } companion object { @@ -125,21 +200,34 @@ class MemberLeaveEventHandlerTest { val conversationId = ConversationId("conversationId", "domain") val list = listOf(qualifiedUserIdEntity) - val memberLeaveEvent = Event.Conversation.MemberLeave( + fun memberLeaveEvent(reason: MemberLeaveReason) = Event.Conversation.MemberLeave( id = "id", conversationId = conversationId, transient = false, live = false, removedBy = userId, removedList = listOf(userId), - timestampIso = "timestampIso" + timestampIso = "timestampIso", + reason = reason + ) + + fun memberRemovedMessage(event: Event.Conversation.MemberLeave) = Message.System( + id = event.id, + content = MessageContent.MemberChange.Removed(members = event.removedList), + conversationId = event.conversationId, + date = event.timestampIso, + senderUserId = event.removedBy, + status = Message.Status.Sent, + visibility = Message.Visibility.VISIBLE, + expirationData = null ) - val message = Message.System( - id = memberLeaveEvent.id, - content = MessageContent.MemberChange.Removed(members = memberLeaveEvent.removedList), - conversationId = memberLeaveEvent.conversationId, - date = memberLeaveEvent.timestampIso, - senderUserId = memberLeaveEvent.removedBy, + + fun memberRemovedFromTeamMessage(event: Event.Conversation.MemberLeave) = Message.System( + id = event.id, + content = MessageContent.MemberChange.RemovedFromTeam(members = event.removedList), + conversationId = event.conversationId, + date = event.timestampIso, + senderUserId = event.removedBy, status = Message.Status.Sent, visibility = Message.Visibility.VISIBLE, expirationData = null diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/UserRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/UserRepositoryArrangement.kt deleted file mode 100644 index 3f9d74dfc6f..00000000000 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/UserRepositoryArrangement.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Wire - * Copyright (C) 2023 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.kalium.logic.util.arrangement - -import com.wire.kalium.logic.CoreFailure -import com.wire.kalium.logic.data.user.OtherUser -import com.wire.kalium.logic.data.user.SelfUser -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.functional.Either -import io.mockative.Mock -import io.mockative.any -import io.mockative.given -import io.mockative.matchers.Matcher -import io.mockative.mock -import kotlinx.coroutines.flow.Flow - -internal interface UserRepositoryArrangement { - val userRepository: UserRepository - - fun withUpdateUserSuccess() - - fun withUpdateUserFailure(coreFailure: CoreFailure) - - fun withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess() - - fun withSelfUserReturning(selfUser: SelfUser?) - - fun withUserByIdReturning(result: Either) - - fun withUpdateOneOnOneConversationReturning(result: Either) - - fun withGetKnownUserReturning(result: Flow) - - fun withGetUsersWithOneOnOneConversationReturning(result: List) - - fun withFetchAllOtherUsersReturning(result: Either) - - fun withFetchUserInfoReturning(result: Either) - - fun withFetchUsersByIdReturning( - result: Either, - userIdList: Matcher> = any() - ) -} - -internal class UserRepositoryArrangementImpl: UserRepositoryArrangement { - - @Mock - override val userRepository: UserRepository = mock(UserRepository::class) - - override fun withUpdateUserSuccess() { - given(userRepository).suspendFunction(userRepository::updateUserFromEvent).whenInvokedWith(any()) - .thenReturn(Either.Right(Unit)) - } - - override fun withUpdateUserFailure(coreFailure: CoreFailure) { - given(userRepository).suspendFunction(userRepository::updateUserFromEvent) - .whenInvokedWith(any()).thenReturn(Either.Left(coreFailure)) - } - - override fun withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess() { - given(userRepository).suspendFunction(userRepository::markUserAsDeletedAndRemoveFromGroupConversations) - .whenInvokedWith(any()).thenReturn(Either.Right(Unit)) - } - - override fun withSelfUserReturning(selfUser: SelfUser?) { - given(userRepository) - .suspendFunction(userRepository::getSelfUser) - .whenInvoked() - .thenReturn(selfUser) - } - - override fun withUserByIdReturning(result: Either) { - given(userRepository) - .suspendFunction(userRepository::userById) - .whenInvokedWith(any()) - .thenReturn(result) - } - - override fun withUpdateOneOnOneConversationReturning(result: Either) { - given(userRepository) - .suspendFunction(userRepository::updateActiveOneOnOneConversation) - .whenInvokedWith(any()) - .thenReturn(result) - } - - override fun withGetKnownUserReturning(result: Flow) { - given(userRepository) - .suspendFunction(userRepository::getKnownUser) - .whenInvokedWith(any()) - .thenReturn(result) - } - - override fun withGetUsersWithOneOnOneConversationReturning(result: List) { - given(userRepository) - .suspendFunction(userRepository::getUsersWithOneOnOneConversation) - .whenInvoked() - .thenReturn(result) - } - - override fun withFetchAllOtherUsersReturning(result: Either) { - given(userRepository) - .suspendFunction(userRepository::fetchAllOtherUsers) - .whenInvoked() - .thenReturn(result) - } - - override fun withFetchUserInfoReturning(result: Either) { - given(userRepository) - .suspendFunction(userRepository::fetchUserInfo) - .whenInvokedWith(any()) - .thenReturn(result) - } - - override fun withFetchUsersByIdReturning( - result: Either, - userIdList: Matcher> - ) { - given(userRepository) - .suspendFunction(userRepository::fetchUsersByIds) - .whenInvokedWith(userIdList) - .thenReturn(result) - } - - -} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/dao/MemberDAOArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/dao/MemberDAOArrangement.kt index bf67a3a1eb8..6d61968a30b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/dao/MemberDAOArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/dao/MemberDAOArrangement.kt @@ -67,6 +67,7 @@ interface MemberDAOArrangement { ) fun withDeleteMembersByQualifiedID( + throws: Throwable? = null, conversationId: Matcher = any(), memberIdList: Matcher> = any() ) @@ -140,13 +141,21 @@ class MemberDAOArrangementImpl : MemberDAOArrangement { } override fun withDeleteMembersByQualifiedID( + throws: Throwable?, conversationId: Matcher, memberIdList: Matcher> ) { - given(memberDAO) - .suspendFunction(memberDAO::deleteMembersByQualifiedID) - .whenInvokedWith(memberIdList, conversationId) - .thenReturn(Unit) + if (throws != null) { + given(memberDAO) + .suspendFunction(memberDAO::deleteMembersByQualifiedID) + .whenInvokedWith(memberIdList, conversationId) + .thenThrow(throws) + } else { + given(memberDAO) + .suspendFunction(memberDAO::deleteMembersByQualifiedID) + .whenInvokedWith(memberIdList, conversationId) + .thenReturn(Unit) + } } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt index 07c3abc9594..d732baa0e2b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt @@ -18,11 +18,15 @@ package com.wire.kalium.logic.util.arrangement.repository import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.user.OtherUser +import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.User import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either +import io.mockative.KFunction1 import io.mockative.Mock import io.mockative.any import io.mockative.given @@ -30,13 +34,59 @@ import io.mockative.matchers.Matcher import io.mockative.mock import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf +import kotlin.jvm.JvmName +@Suppress("INAPPLICABLE_JVM_NAME") internal interface UserRepositoryArrangement { val userRepository: UserRepository fun withDefederateUser(result: Either, userId: Matcher = any()) fun withObserveUser(result: Flow = flowOf(TestUser.OTHER), userId: Matcher = any()) + + fun withUpdateUserSuccess() + + fun withUpdateUserFailure(coreFailure: CoreFailure) + + @JvmName("withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccessWithUserId") + fun withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess( + userIdMatcher: Matcher = any() + ) + + @JvmName("withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccessWithUserIdList") + fun withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess( + userIdMatcher: Matcher> = any() + ) + + fun withSelfUserReturning(selfUser: SelfUser?) + + fun withUserByIdReturning(result: Either) + + fun withUpdateOneOnOneConversationReturning(result: Either) + + fun withGetKnownUserReturning(result: Flow) + + fun withGetUsersWithOneOnOneConversationReturning(result: List) + + fun withFetchAllOtherUsersReturning(result: Either) + + fun withFetchUserInfoReturning(result: Either) + + fun withFetchUsersByIdReturning( + result: Either, + userIdList: Matcher> = any() + ) + + fun withFetchUsersIfUnknownByIdsReturning( + result: Either, + userIdList: Matcher> = any() + ) + + fun withIsAtLeastOneUserATeamMember( + result: Either, + userIdList: Matcher> = any() + ) } +@Suppress("INAPPLICABLE_JVM_NAME") internal open class UserRepositoryArrangementImpl : UserRepositoryArrangement { @Mock override val userRepository: UserRepository = mock(UserRepository::class) @@ -57,4 +107,107 @@ internal open class UserRepositoryArrangementImpl : UserRepositoryArrangement { .whenInvokedWith(userId) .thenReturn(result) } + + override fun withUpdateUserSuccess() { + given(userRepository).suspendFunction(userRepository::updateUserFromEvent).whenInvokedWith(any()) + .thenReturn(Either.Right(Unit)) + } + + override fun withUpdateUserFailure(coreFailure: CoreFailure) { + given(userRepository).suspendFunction(userRepository::updateUserFromEvent) + .whenInvokedWith(any()).thenReturn(Either.Left(coreFailure)) + } + + @JvmName("withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccessWithUserId") + override fun withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess( + userIdMatcher: Matcher + ) { + given(userRepository).suspendFunction( + userRepository::markUserAsDeletedAndRemoveFromGroupConversations, + KFunction1() + ).whenInvokedWith(userIdMatcher) + .thenReturn(Either.Right(Unit)) + } + + @JvmName("withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccessWithUserIdList") + override fun withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess(userIdMatcher: Matcher>) { + given(userRepository).suspendFunction( + userRepository::markUserAsDeletedAndRemoveFromGroupConversations, + KFunction1>() + ).whenInvokedWith(userIdMatcher) + .thenReturn(Either.Right(Unit)) + } + + override fun withSelfUserReturning(selfUser: SelfUser?) { + given(userRepository) + .suspendFunction(userRepository::getSelfUser) + .whenInvoked() + .thenReturn(selfUser) + } + + override fun withUserByIdReturning(result: Either) { + given(userRepository) + .suspendFunction(userRepository::userById) + .whenInvokedWith(any()) + .thenReturn(result) + } + + override fun withUpdateOneOnOneConversationReturning(result: Either) { + given(userRepository) + .suspendFunction(userRepository::updateActiveOneOnOneConversation) + .whenInvokedWith(any()) + .thenReturn(result) + } + + override fun withGetKnownUserReturning(result: Flow) { + given(userRepository) + .suspendFunction(userRepository::getKnownUser) + .whenInvokedWith(any()) + .thenReturn(result) + } + + override fun withGetUsersWithOneOnOneConversationReturning(result: List) { + given(userRepository) + .suspendFunction(userRepository::getUsersWithOneOnOneConversation) + .whenInvoked() + .thenReturn(result) + } + + override fun withFetchAllOtherUsersReturning(result: Either) { + given(userRepository) + .suspendFunction(userRepository::fetchAllOtherUsers) + .whenInvoked() + .thenReturn(result) + } + + override fun withFetchUserInfoReturning(result: Either) { + given(userRepository) + .suspendFunction(userRepository::fetchUserInfo) + .whenInvokedWith(any()) + .thenReturn(result) + } + + override fun withFetchUsersByIdReturning( + result: Either, + userIdList: Matcher> + ) { + given(userRepository) + .suspendFunction(userRepository::fetchUsersByIds) + .whenInvokedWith(userIdList) + .thenReturn(result) + } + + override fun withFetchUsersIfUnknownByIdsReturning(result: Either, userIdList: Matcher>) { + given(userRepository) + .suspendFunction(userRepository::fetchUsersIfUnknownByIds) + .whenInvokedWith(userIdList) + .thenReturn(result) + } + + override fun withIsAtLeastOneUserATeamMember(result: Either, userIdList: Matcher>) { + given(userRepository) + .suspendFunction(userRepository::isAtLeastOneUserATeamMember) + .whenInvokedWith(userIdList) + .thenReturn(result) + } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/usecase/PersistMessageUseCaseArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/usecase/PersistMessageUseCaseArrangement.kt index 548751b020c..395cfd9a5cc 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/usecase/PersistMessageUseCaseArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/usecase/PersistMessageUseCaseArrangement.kt @@ -18,26 +18,34 @@ package com.wire.kalium.logic.util.arrangement.usecase import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.PersistMessageUseCase import com.wire.kalium.logic.functional.Either import io.mockative.Mock import io.mockative.any import io.mockative.given +import io.mockative.matchers.Matcher import io.mockative.mock internal interface PersistMessageUseCaseArrangement { val persistMessageUseCase: PersistMessageUseCase - fun withPersistingMessage(result: Either): PersistMessageUseCaseArrangementImpl + fun withPersistingMessage( + result: Either, + messageMatcher: Matcher = any() + ): PersistMessageUseCaseArrangementImpl } internal open class PersistMessageUseCaseArrangementImpl : PersistMessageUseCaseArrangement { @Mock override val persistMessageUseCase: PersistMessageUseCase = mock(PersistMessageUseCase::class) - override fun withPersistingMessage(result: Either) = apply { + override fun withPersistingMessage( + result: Either, + messageMatcher: Matcher + ) = apply { given(persistMessageUseCase) .suspendFunction(persistMessageUseCase::invoke) - .whenInvokedWith(any()) + .whenInvokedWith(messageMatcher) .thenReturn(result) } } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/conversation/ConversationEvent.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/conversation/ConversationEvent.kt index a500b88e90e..86fb1c0a6a9 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/conversation/ConversationEvent.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/conversation/ConversationEvent.kt @@ -20,6 +20,7 @@ package com.wire.kalium.network.api.base.authenticated.conversation +import com.wire.kalium.network.api.base.authenticated.notification.MemberLeaveReasonDTO import com.wire.kalium.network.api.base.model.UserId import kotlinx.serialization.EncodeDefault import kotlinx.serialization.ExperimentalSerializationApi @@ -27,16 +28,15 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ConversationMembers( +data class ConversationMembers @OptIn(ExperimentalSerializationApi::class) constructor( @SerialName("user_ids") val userIds: List, @EncodeDefault @SerialName("users") val users: List = emptyList() ) @Serializable -data class ConversationUsers( - @Deprecated("use qualifiedUserIds", replaceWith = ReplaceWith("this.qualifiedUserIds")) - @SerialName("user_ids") val userIds: List, - @SerialName("qualified_user_ids") val qualifiedUserIds: List +data class ConversationMemberRemovedDTO( + @SerialName("qualified_user_ids") val qualifiedUserIds: List, + @SerialName("reason") val reason: MemberLeaveReasonDTO = MemberLeaveReasonDTO.LEFT ) @Serializable diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/EventContentDTO.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/EventContentDTO.kt index f868c30a175..1d387be6464 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/EventContentDTO.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/EventContentDTO.kt @@ -21,11 +21,11 @@ package com.wire.kalium.network.api.base.authenticated.notification import com.wire.kalium.network.api.base.authenticated.client.ClientDTO import com.wire.kalium.network.api.base.authenticated.client.ClientIdDTO import com.wire.kalium.network.api.base.authenticated.connection.ConnectionDTO +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMemberRemovedDTO import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMembers import com.wire.kalium.network.api.base.authenticated.conversation.ConversationNameUpdateEvent import com.wire.kalium.network.api.base.authenticated.conversation.ConversationResponse import com.wire.kalium.network.api.base.authenticated.conversation.ConversationRoleChange -import com.wire.kalium.network.api.base.authenticated.conversation.ConversationUsers import com.wire.kalium.network.api.base.authenticated.conversation.TypingIndicatorStatusDTO import com.wire.kalium.network.api.base.authenticated.conversation.guestroomlink.ConversationInviteLinkResponse import com.wire.kalium.network.api.base.authenticated.conversation.messagetimer.ConversationMessageTimerDTO @@ -140,7 +140,7 @@ sealed class EventContentDTO { data class NewConversationDTO( @SerialName("qualified_conversation") val qualifiedConversation: ConversationId, @SerialName("qualified_from") val qualifiedFrom: UserId, - val time: String, + @SerialName("time") val time: String, @SerialName("data") val data: ConversationResponse, ) : Conversation() @@ -157,7 +157,7 @@ sealed class EventContentDTO { data class ConversationRenameDTO( @SerialName("qualified_conversation") val qualifiedConversation: ConversationId, @SerialName("qualified_from") val qualifiedFrom: UserId, - val time: String, + @SerialName("time") val time: String, @SerialName("data") val updateNameData: ConversationNameUpdateEvent, ) : Conversation() @@ -166,7 +166,7 @@ sealed class EventContentDTO { data class MemberJoinDTO( @SerialName("qualified_conversation") val qualifiedConversation: ConversationId, @SerialName("qualified_from") val qualifiedFrom: UserId, - val time: String, + @SerialName("time") val time: String, @SerialName("data") val members: ConversationMembers, @Deprecated("use qualifiedFrom", replaceWith = ReplaceWith("this.qualifiedFrom")) @SerialName("from") val from: String ) : Conversation() @@ -176,9 +176,8 @@ sealed class EventContentDTO { data class MemberLeaveDTO( @SerialName("qualified_conversation") val qualifiedConversation: ConversationId, @SerialName("qualified_from") val qualifiedFrom: UserId, - val time: String, - // TODO: rename members to something else since the name is confusing (it's only userIDs) - @SerialName("data") val members: ConversationUsers, + @SerialName("time") val time: String, + @SerialName("data") val removedUsers: ConversationMemberRemovedDTO, @SerialName("from") val from: String ) : Conversation() @@ -187,7 +186,7 @@ sealed class EventContentDTO { data class MemberUpdateDTO( @SerialName("qualified_conversation") val qualifiedConversation: ConversationId, @SerialName("qualified_from") val qualifiedFrom: UserId, - val time: String, + @SerialName("time") val time: String, @SerialName("from") val from: String, @SerialName("data") val roleChange: ConversationRoleChange ) : Conversation() @@ -197,7 +196,7 @@ sealed class EventContentDTO { data class ConversationTypingDTO( @SerialName("qualified_conversation") val qualifiedConversation: ConversationId, @SerialName("qualified_from") val qualifiedFrom: UserId, - val time: String, + @SerialName("time") val time: String, @SerialName("from") val from: String, @SerialName("data") val status: TypingIndicatorStatusDTO, ) : Conversation() @@ -207,7 +206,7 @@ sealed class EventContentDTO { data class NewMessageDTO( @SerialName("qualified_conversation") val qualifiedConversation: ConversationId, @SerialName("qualified_from") val qualifiedFrom: UserId, - val time: String, + @SerialName("time") val time: String, @SerialName("data") val data: MessageEventData, ) : Conversation() @@ -256,7 +255,7 @@ sealed class EventContentDTO { data class NewMLSMessageDTO( @SerialName("qualified_conversation") val qualifiedConversation: ConversationId, @SerialName("qualified_from") val qualifiedFrom: UserId, - val time: String, + @SerialName("time") val time: String, @SerialName("data") val message: String, @SerialName("subconv") val subconversation: String?, ) : Conversation() @@ -287,7 +286,7 @@ sealed class EventContentDTO { data class Update( @SerialName("data") val teamUpdate: TeamUpdateData, @SerialName("team") val teamId: TeamId, - val time: String, + @SerialName("time") val time: String, ) : Team() @Serializable @@ -295,7 +294,7 @@ sealed class EventContentDTO { data class MemberUpdate( @SerialName("data") val permissionsResponse: PermissionsData, @SerialName("team") val teamId: TeamId, - val time: String, + @SerialName("time") val time: String, ) : Team() } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/MemberLeaveReasonDTO.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/MemberLeaveReasonDTO.kt new file mode 100644 index 00000000000..d4e2691f9c4 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/MemberLeaveReasonDTO.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.base.authenticated.notification + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class MemberLeaveReasonDTO { + @SerialName("left") + LEFT, + + @SerialName("removed") + REMOVED, + + @SerialName("user-deleted") + USER_DELETED; + + override fun toString(): String { + return when (this) { + LEFT -> "left" + REMOVED -> "removed" + USER_DELETED -> "user-deleted" + } + } +} diff --git a/network/src/commonTest/kotlin/com/wire/kalium/model/EventContentDTOJson.kt b/network/src/commonTest/kotlin/com/wire/kalium/model/EventContentDTOJson.kt index 83c7512d705..ceaf9b8043c 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/model/EventContentDTOJson.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/model/EventContentDTOJson.kt @@ -20,13 +20,14 @@ package com.wire.kalium.model import com.wire.kalium.api.json.ValidJsonProvider import com.wire.kalium.network.api.base.authenticated.conversation.ConvProtocol +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMemberRemovedDTO import com.wire.kalium.network.api.base.authenticated.conversation.ConversationMembers -import com.wire.kalium.network.api.base.authenticated.conversation.ConversationUsers import com.wire.kalium.network.api.base.authenticated.conversation.ReceiptMode import com.wire.kalium.network.api.base.authenticated.conversation.model.ConversationAccessInfoDTO import com.wire.kalium.network.api.base.authenticated.conversation.model.ConversationProtocolDTO import com.wire.kalium.network.api.base.authenticated.conversation.model.ConversationReceiptModeDTO import com.wire.kalium.network.api.base.authenticated.notification.EventContentDTO +import com.wire.kalium.network.api.base.authenticated.notification.MemberLeaveReasonDTO import com.wire.kalium.network.api.base.model.ConversationAccessDTO import com.wire.kalium.network.api.base.model.ConversationAccessRoleDTO import com.wire.kalium.network.api.base.model.ConversationId @@ -203,7 +204,7 @@ object EventContentDTOJson { qualifiedFrom = UserId("ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", "anta.wire.link"), from = "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", time = "2021-05-31T10:52:02.671Z", - members = ConversationUsers(emptyList(), emptyList()) + removedUsers = ConversationMemberRemovedDTO(emptyList(), MemberLeaveReasonDTO.LEFT) ), jsonProviderMemberLeave ) diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq index 392d8c0ec0a..c099ac3a533 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq @@ -85,7 +85,7 @@ markUserAsDeleted: INSERT INTO User(qualified_id, user_type, deleted) VALUES (:qualified_id, :user_type, 1) ON CONFLICT (qualified_id) DO UPDATE SET -team = NULL , preview_asset_id = NULL, complete_asset_id = NULL, user_type = excluded.user_type, deleted = 1; +preview_asset_id = NULL, complete_asset_id = NULL, user_type = excluded.user_type, deleted = 1; deleteUserFromGroupConversations: DELETE FROM Member @@ -237,3 +237,9 @@ UPDATE User SET supported_protocols = ? WHERE qualified_id = ?; updateOneOnOnConversationId: UPDATE User SET active_one_on_one_conversation_id = ? WHERE qualified_id = ?; + +isOneUserATeamMember: +SELECT EXISTS ( + SELECT 1 FROM User + WHERE qualified_id IN :userIdList AND team = :teamId +); diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt index 7178236b6bb..23212eeccb8 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt @@ -267,6 +267,7 @@ interface UserDAO { suspend fun deleteUserByQualifiedID(qualifiedID: QualifiedIDEntity) suspend fun markUserAsDeletedAndRemoveFromGroupConv(qualifiedID: QualifiedIDEntity) + suspend fun markUserAsDeletedAndRemoveFromGroupConv(qualifiedID: List) suspend fun markUserAsDefederated(qualifiedID: QualifiedIDEntity) suspend fun updateUserHandle(qualifiedID: QualifiedIDEntity, handle: String) suspend fun updateUserAvailabilityStatus(qualifiedID: QualifiedIDEntity, status: UserAvailabilityStatusEntity) @@ -300,4 +301,5 @@ interface UserDAO { suspend fun updateActiveOneOnOneConversation(userId: QualifiedIDEntity, conversationId: QualifiedIDEntity) suspend fun upsertConnectionStatuses(userStatuses: Map) + suspend fun isAtLeastOneUserATeamMember(userId: List, teamId: String): Boolean } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt index 7b9b15da622..784e0602538 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt @@ -304,6 +304,14 @@ class UserDAOImpl internal constructor( userQueries.deleteUser(qualifiedID) } + override suspend fun markUserAsDeletedAndRemoveFromGroupConv(qualifiedID: List) = withContext(queriesContext) { + userQueries.transaction { + qualifiedID.forEach { + safeMarkAsDeleted(it) + } + } + } + override suspend fun markUserAsDeletedAndRemoveFromGroupConv(qualifiedID: QualifiedIDEntity) = withContext(queriesContext) { userQueries.transaction { safeMarkAsDeleted(qualifiedID) @@ -412,4 +420,8 @@ class UserDAOImpl internal constructor( } } } + + override suspend fun isAtLeastOneUserATeamMember(userId: List, teamId: String): Boolean = withContext(queriesContext) { + userQueries.isOneUserATeamMember(userId, teamId).executeAsOneOrNull() ?: false + } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt index 2b1c13c191e..5bdf417f5da 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt @@ -202,7 +202,7 @@ sealed interface MessageEntity { } enum class MemberChangeType { - ADDED, REMOVED, CREATION_ADDED, FAILED_TO_ADD, FEDERATION_REMOVED + ADDED, REMOVED, CREATION_ADDED, FAILED_TO_ADD, FEDERATION_REMOVED, REMOVED_FROM_TEAM; } enum class FederationType { @@ -412,7 +412,12 @@ sealed class MessagePreviewEntityContent { val isContainSelfUserId: Boolean, ) : MessagePreviewEntityContent() - data class MembersRemoved( + data class ConversationMembersRemoved( + val senderName: String?, + val otherUserIdList: List, + val isContainSelfUserId: Boolean, + ) : MessagePreviewEntityContent() + data class TeamMembersRemoved( val senderName: String?, val otherUserIdList: List, val isContainSelfUserId: Boolean, @@ -440,7 +445,10 @@ sealed class MessagePreviewEntityContent { data class MemberLeft(val senderName: String?) : MessagePreviewEntityContent() data class ConversationNameChange(val adminName: String?) : MessagePreviewEntityContent() - data class TeamMemberRemoved(val userName: String?) : MessagePreviewEntityContent() + + @Deprecated("not maintained and will be deleted") + @Suppress("ClassNaming") + data class TeamMemberRemoved_Legacy(val userName: String?) : MessagePreviewEntityContent() data class Ephemeral(val isGroupConversation: Boolean) : MessagePreviewEntityContent() data object CryptoSessionReset : MessagePreviewEntityContent() data object ConversationVerifiedMls : MessagePreviewEntityContent() diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt index d65a51f9856..726bc04ada0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt @@ -147,7 +147,7 @@ object MessageMapper { if (userIdList.contains(senderUserId) && userIdList.size == 1) { MessagePreviewEntityContent.MemberLeft(senderName) } else { - MessagePreviewEntityContent.MembersRemoved( + MessagePreviewEntityContent.ConversationMembersRemoved( senderName = senderName, isContainSelfUserId = userIdList .firstOrNull { it.value == selfUserId?.value }?.let { true } ?: false, @@ -175,6 +175,13 @@ object MessageMapper { .firstOrNull { it.value == selfUserId?.value }?.let { true } ?: false, otherUserIdList = userIdList.filterNot { it == selfUserId }, ) + + MessageEntity.MemberChangeType.REMOVED_FROM_TEAM -> MessagePreviewEntityContent.TeamMembersRemoved( + senderName = senderName, + isContainSelfUserId = userIdList + .firstOrNull { it.value == selfUserId?.value }?.let { true } ?: false, + otherUserIdList = userIdList.filterNot { it == selfUserId }, + ) } } @@ -188,7 +195,7 @@ object MessageMapper { adminName = senderName ) - MessageEntity.ContentType.REMOVED_FROM_TEAM -> MessagePreviewEntityContent.TeamMemberRemoved(userName = senderName) + MessageEntity.ContentType.REMOVED_FROM_TEAM -> MessagePreviewEntityContent.TeamMemberRemoved_Legacy(userName = senderName) MessageEntity.ContentType.LOCATION -> MessagePreviewEntityContent.Location(senderName = senderName) MessageEntity.ContentType.FEDERATION -> MessagePreviewEntityContent.Unknown MessageEntity.ContentType.NEW_CONVERSATION_RECEIPT_MODE -> MessagePreviewEntityContent.Unknown diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/UserDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/UserDAOTest.kt index c7c6837eade..2437446f6cd 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/UserDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/UserDAOTest.kt @@ -479,7 +479,7 @@ class UserDAOTest : BaseDatabaseTest() { // when db.userDAO.observeUserDetailsByQualifiedID(USER_ENTITY_1.id).first().also { searchResult -> // then - assertEquals(mockUser.copy(deleted = true, team = null, userType = UserTypeEntity.NONE), searchResult?.toSimpleEntity()) + assertEquals(mockUser.copy(deleted = true, userType = UserTypeEntity.NONE), searchResult?.toSimpleEntity()) } } @@ -669,7 +669,7 @@ class UserDAOTest : BaseDatabaseTest() { fun givenUser_WhenMarkingAsDeleted_ThenProperValueShouldBeUpdated() = runTest(dispatcher) { val user = user1 db.userDAO.upsertUser(user) - val deletedUser = user1.copy(deleted = true, team = null, userType = UserTypeEntity.NONE) + val deletedUser = user1.copy(deleted = true, userType = UserTypeEntity.NONE) db.userDAO.markUserAsDeletedAndRemoveFromGroupConv(user1.id) val result = db.userDAO.observeUserDetailsByQualifiedID(user1.id).first() assertEquals(result?.toSimpleEntity(), deletedUser) @@ -911,6 +911,38 @@ class UserDAOTest : BaseDatabaseTest() { assertNotEquals(updatedTeamMemberUser.activeOneOnOneConversationId, result.activeOneOnOneConversationId) } + @Test + fun givenListOfUsers_whenOnlyOneBelongsToTheTeam_thenReturnTrue() = runTest { + val teamId = "teamId" + val users = listOf( + newUserEntity().copy(team = teamId, id = UserIDEntity("1", "wire.com")), + newUserEntity().copy(team = null, id = UserIDEntity("2", "wire.com")), + newUserEntity().copy(team = null, id = UserIDEntity("3", "wire.com")), + newUserEntity().copy(team = null, id = UserIDEntity("4", "wire.com")), + newUserEntity().copy(team = null, id = UserIDEntity("5", "wire.com")), + ) + + db.userDAO.upsertUsers(users) + + assertTrue { db.userDAO.isAtLeastOneUserATeamMember(users.map { it.id }, teamId) } + } + + @Test + fun givenListOfUsers_whenNoneBelongsToTheTeam_thenReturnFalse() = runTest { + val teamId = "teamId" + val users = listOf( + newUserEntity().copy(team = null, id = UserIDEntity("1", "wire.com")), + newUserEntity().copy(team = null, id = UserIDEntity("2", "wire.com")), + newUserEntity().copy(team = null, id = UserIDEntity("3", "wire.com")), + newUserEntity().copy(team = null, id = UserIDEntity("4", "wire.com")), + newUserEntity().copy(team = null, id = UserIDEntity("5", "wire.com")), + ) + + db.userDAO.upsertUsers(users) + + assertFalse { db.userDAO.isAtLeastOneUserATeamMember(users.map { it.id }, teamId) } + } + private companion object { val USER_ENTITY_1 = newUserEntity(QualifiedIDEntity("1", "wire.com")) val USER_ENTITY_2 = newUserEntity(QualifiedIDEntity("2", "wire.com"))