From 012fdab6bec085af6c1945ca79d403069354647a Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 24 Jan 2025 10:53:37 +0100 Subject: [PATCH 1/4] fix: sub conversation epoch verification --- .../data/conversation/SubConversation.kt | 39 +++++ .../LeaveSubconversationUseCase.kt | 4 +- .../conversation/SubConversationMapper.kt | 43 ++++++ .../conversation/SubconversationRepository.kt | 9 +- .../wire/kalium/logic/data/id/IdMappers.kt | 3 + .../kalium/logic/feature/UserSessionScope.kt | 3 +- .../feature/message/StaleEpochVerifier.kt | 53 ++++++- .../message/NewMessageEventHandler.kt | 1 + .../feature/message/StaleEpochVerifierTest.kt | 141 +++++++++++++++++- .../message/NewMessageEventHandlerTest.kt | 6 +- .../MLSConversationRepositoryArrangement.kt | 12 ++ .../SubconversationRepositoryArrangement.kt | 137 +++++++++++++++++ .../conversation/ConversationResponse.kt | 4 +- 13 files changed, 438 insertions(+), 17 deletions(-) create mode 100644 data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubConversation.kt create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubConversationMapper.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/SubconversationRepositoryArrangement.kt diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubConversation.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubConversation.kt new file mode 100644 index 00000000000..d9a9af3088a --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubConversation.kt @@ -0,0 +1,39 @@ +/* + * Wire + * Copyright (C) 2025 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.conversation + +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.GroupID +import com.wire.kalium.logic.data.id.SubconversationId + +data class SubConversation( + val id: SubconversationId, + val parentId: ConversationId, + val groupId: GroupID, + val epoch: ULong, + val epochTimestamp: String?, + val mlsCipherSuiteTag: Int?, + + val members: List, +) + +data class SubconversationMember( + val clientId: String, + val userId: String, + val domain: String +) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/LeaveSubconversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/LeaveSubconversationUseCase.kt index ec549636be4..997158f21eb 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/LeaveSubconversationUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/LeaveSubconversationUseCase.kt @@ -31,7 +31,7 @@ import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.logic.wrapMLSRequest import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi -import com.wire.kalium.network.api.authenticated.conversation.SubconversationMember +import com.wire.kalium.network.api.authenticated.conversation.SubconversationMemberDTO /** * Leave a sub-conversation you've previously joined @@ -71,7 +71,7 @@ internal class LeaveSubconversationUseCaseImpl( subconversationRepository.getSubconversationInfo(conversationId, subconversationId)?.let { Either.Right(it) } ?: wrapApiRequest { conversationApi.fetchSubconversationDetails(conversationId.toApi(), subconversationId.toApi()) }.flatMap { - if (it.members.contains(SubconversationMember(selfClientId.value, selfUserId.value, selfUserId.domain))) { + if (it.members.contains(SubconversationMemberDTO(selfClientId.value, selfUserId.value, selfUserId.domain))) { Either.Right(GroupID(it.groupId)) } else { Either.Right(null) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubConversationMapper.kt new file mode 100644 index 00000000000..7ea14401a8e --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubConversationMapper.kt @@ -0,0 +1,43 @@ +/* + * Wire + * Copyright (C) 2025 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.conversation + +import com.wire.kalium.logic.data.id.GroupID +import com.wire.kalium.logic.data.id.toModel +import com.wire.kalium.network.api.authenticated.conversation.SubconversationMemberDTO +import com.wire.kalium.network.api.authenticated.conversation.SubconversationResponse + +fun SubconversationResponse.toModel(): SubConversation { + return SubConversation( + id = id.toModel(), + parentId = parentId.toModel(), + groupId = GroupID(groupId), + epoch = epoch, + epochTimestamp = epochTimestamp, + mlsCipherSuiteTag = mlsCipherSuiteTag, + members = members.map { it.toModel() } + ) +} + +fun SubconversationMemberDTO.toModel(): SubconversationMember { + return SubconversationMember( + clientId = clientId, + userId = userId, + domain = domain + ) +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt index d6281310b22..84c1fc7ac70 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt @@ -24,12 +24,12 @@ import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.id.SubconversationId import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.logic.functional.onSuccess import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.network.api.authenticated.conversation.SubconversationDeleteRequest -import com.wire.kalium.network.api.authenticated.conversation.SubconversationResponse import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import io.ktor.util.collections.ConcurrentMap import kotlinx.coroutines.sync.Mutex @@ -68,7 +68,7 @@ interface SubconversationRepository { suspend fun fetchRemoteSubConversationDetails( conversationId: ConversationId, subConversationId: SubconversationId - ): Either + ): Either } class SubconversationRepositoryImpl( @@ -139,14 +139,15 @@ class SubconversationRepositoryImpl( ) } - // TODO: Replace SubconversationResponse with a domain model override suspend fun fetchRemoteSubConversationDetails( conversationId: ConversationId, subConversationId: SubconversationId - ): Either = wrapApiRequest { + ): Either = wrapApiRequest { conversationApi.fetchSubconversationDetails( conversationId.toApi(), subConversationId.toApi() ) + }.map { + it.toModel() } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/id/IdMappers.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/id/IdMappers.kt index f62eca32465..1b34b69f415 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/id/IdMappers.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/id/IdMappers.kt @@ -26,6 +26,7 @@ import com.wire.kalium.cryptography.MLSGroupId import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.network.api.model.UserAssetDTO import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.network.api.model.SubconversationId as NetworkSubConversationId internal typealias NetworkQualifiedId = com.wire.kalium.network.api.model.QualifiedID internal typealias PersistenceQualifiedId = QualifiedIDEntity @@ -53,3 +54,5 @@ internal fun SubconversationId.toApi(): String = value internal fun GroupID.toCrypto(): MLSGroupId = value internal fun CryptoQualifiedClientId.toModel() = QualifiedClientID(ClientId(value), userId.toModel()) + +internal fun NetworkSubConversationId.toModel() = SubconversationId(this) 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 50f03e1294e..55409c4cb05 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 @@ -1392,7 +1392,8 @@ class UserSessionScope internal constructor( systemMessageInserter = systemMessageInserter, conversationRepository = conversationRepository, mlsConversationRepository = mlsConversationRepository, - joinExistingMLSConversation = joinExistingMLSConversationUseCase + joinExistingMLSConversation = joinExistingMLSConversationUseCase, + subconversationRepository = subconversationRepository ) private val newMessageHandler: NewMessageEventHandler diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifier.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifier.kt index e14c2a816ad..b2fe6c42f4a 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifier.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifier.kt @@ -22,10 +22,12 @@ import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.MLSFailure import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.JoinExistingMLSConversationUseCase import com.wire.kalium.logic.data.conversation.MLSConversationRepository +import com.wire.kalium.logic.data.conversation.SubconversationRepository import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.SubconversationId import com.wire.kalium.logic.data.message.SystemMessageInserter -import com.wire.kalium.logic.data.conversation.JoinExistingMLSConversationUseCase import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.map @@ -34,19 +36,36 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant interface StaleEpochVerifier { - suspend fun verifyEpoch(conversationId: ConversationId, timestamp: Instant? = null): Either + suspend fun verifyEpoch( + conversationId: ConversationId, + subConversationId: SubconversationId? = null, + timestamp: Instant? = null + ): Either } internal class StaleEpochVerifierImpl( private val systemMessageInserter: SystemMessageInserter, private val conversationRepository: ConversationRepository, + private val subconversationRepository: SubconversationRepository, private val mlsConversationRepository: MLSConversationRepository, private val joinExistingMLSConversation: JoinExistingMLSConversationUseCase ) : StaleEpochVerifier { private val logger by lazy { kaliumLogger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.MESSAGES) } - override suspend fun verifyEpoch(conversationId: ConversationId, timestamp: Instant?): Either { - logger.i("Verifying stale epoch") + override suspend fun verifyEpoch( + conversationId: ConversationId, + subConversationId: SubconversationId?, + timestamp: Instant? + ): Either { + return if (subConversationId != null) { + verifySubConversationEpoch(conversationId, subConversationId) + } else { + verifyConversationEpoch(conversationId) + } + } + + private suspend fun verifyConversationEpoch(conversationId: ConversationId): Either { + logger.i("Verifying stale epoch for conversation ${conversationId.toLogString()}") return getUpdatedConversationProtocolInfo(conversationId).flatMap { protocol -> if (protocol is Conversation.ProtocolInfo.MLS) { Either.Right(protocol) @@ -74,6 +93,32 @@ internal class StaleEpochVerifierImpl( } } + private suspend fun verifySubConversationEpoch( + conversationId: ConversationId, + subConversationId: SubconversationId + ): Either { + logger.i("Verifying stale epoch for subconversation ${subConversationId.toLogString()}") + return subconversationRepository.fetchRemoteSubConversationDetails(conversationId, subConversationId) + .flatMap { subConversationDetails -> + mlsConversationRepository.isGroupOutOfSync(subConversationDetails.groupId, subConversationDetails.epoch) + .map { epochIsStale -> + epochIsStale + } + .flatMap { hasMissedCommits -> + if (hasMissedCommits) { + logger.w("Epoch stale due to missing commits, joining by external commit") + subconversationRepository.fetchRemoteSubConversationGroupInfo(conversationId, subConversationId) + .flatMap { groupInfo -> + mlsConversationRepository.joinGroupByExternalCommit(subConversationDetails.groupId, groupInfo) + } + } else { + logger.i("Epoch stale due to unprocessed events") + Either.Right(Unit) + } + } + } + } + private suspend fun getUpdatedConversationProtocolInfo(conversationId: ConversationId): Either { return conversationRepository.fetchConversation(conversationId).flatMap { conversationRepository.getConversationProtocolInfo(conversationId) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt index c4ae6921d38..e9f2dcb1fc2 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt @@ -129,6 +129,7 @@ internal class NewMessageEventHandlerImpl( eventLogger.logFailure(it, "protocol" to "MLS", "mlsOutcome" to "OUT_OF_SYNC") staleEpochVerifier.verifyEpoch( event.conversationId, + event.subconversationId, event.messageInstant ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifierTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifierTest.kt index b1bc573e67e..d5dbb6bc799 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifierTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifierTest.kt @@ -17,7 +17,12 @@ */ package com.wire.kalium.logic.feature.message +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.conversation.SubConversation +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.GroupID +import com.wire.kalium.logic.data.id.SubconversationId import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.util.arrangement.SystemMessageInserterArrangement @@ -26,6 +31,8 @@ import com.wire.kalium.logic.util.arrangement.mls.MLSConversationRepositoryArran import com.wire.kalium.logic.util.arrangement.mls.MLSConversationRepositoryArrangementImpl 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.SubconversationRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.SubconversationRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.usecase.JoinExistingMLSConversationUseCaseArrangement import com.wire.kalium.logic.util.arrangement.usecase.JoinExistingMLSConversationUseCaseArrangementImpl import com.wire.kalium.logic.util.shouldFail @@ -133,18 +140,134 @@ class StaleEpochVerifierTest { }.wasInvoked(once) } + @Test + fun givenSubconversationIdAndValidEpoch_WhenVerified_ThenShouldSucceed() = runTest { + val subConversationId = SubconversationId("subconversation-id") + val (arrangement, staleEpochHandler) = arrange { + withFetchRemoteSubConversationDetails(CONVERSATION_ID, subConversationId, Either.Right(TestSubConversationDetails)) + withIsGroupOutOfSync(Either.Right(false)) + } + + val result = staleEpochHandler.verifyEpoch(CONVERSATION_ID, subConversationId, null) + + result.shouldSucceed() + + coVerify { + arrangement.subconversationRepository.fetchRemoteSubConversationDetails(CONVERSATION_ID, subConversationId) + }.wasInvoked(once) + + coVerify { + arrangement.mlsConversationRepository.isGroupOutOfSync( + TestSubConversationDetails.groupId, + TestSubConversationDetails.epoch + ) + }.wasInvoked(once) + } + + @Test + fun givenSubconversationIdAndStaleEpoch_WhenVerified_ThenShouldJoinUsingExternalCommit() = runTest { + val subConversationId = SubconversationId("subconversation-id") + val (arrangement, staleEpochHandler) = arrange { + withFetchRemoteSubConversationDetails(CONVERSATION_ID, subConversationId, Either.Right(TestSubConversationDetails)) + withIsGroupOutOfSync(Either.Right(true)) + withFetchRemoteSubConversationGroupInfo(CONVERSATION_ID, subConversationId, Either.Right(TestGroupInfo)) + withJoinGroupByExternalCommit(TestSubConversationDetails.groupId, TestGroupInfo, Either.Right(Unit)) + } + + val result = staleEpochHandler.verifyEpoch(CONVERSATION_ID, subConversationId, null) + + result.shouldSucceed() + + coVerify { + arrangement.subconversationRepository.fetchRemoteSubConversationDetails(CONVERSATION_ID, subConversationId) + }.wasInvoked(once) + + coVerify { + arrangement.mlsConversationRepository.joinGroupByExternalCommit(TestSubConversationDetails.groupId, TestGroupInfo) + }.wasInvoked(once) + } + + @Test + fun givenSubconversationIdAndFetchDetailsFails_WhenVerified_ThenShouldFail() = runTest { + val subConversationId = SubconversationId("subconversation-id") + val (arrangement, staleEpochHandler) = arrange { + withFetchRemoteSubConversationDetails( + CONVERSATION_ID, + subConversationId, + Either.Left(NetworkFailure.NoNetworkConnection(null)) + ) + } + + val result = staleEpochHandler.verifyEpoch(CONVERSATION_ID, subConversationId, null) + + result.shouldFail() + + coVerify { + arrangement.subconversationRepository.fetchRemoteSubConversationDetails(CONVERSATION_ID, subConversationId) + }.wasInvoked(once) + } + + @Test + fun givenSubconversationIdAndExternalCommitFails_WhenVerified_ThenShouldFail() = runTest { + val subConversationId = SubconversationId("subconversation-id") + val (arrangement, staleEpochHandler) = arrange { + withFetchRemoteSubConversationDetails(CONVERSATION_ID, subConversationId, Either.Right(TestSubConversationDetails)) + withIsGroupOutOfSync(Either.Right(true)) + withFetchRemoteSubConversationGroupInfo(CONVERSATION_ID, subConversationId, Either.Right(TestGroupInfo)) + withJoinGroupByExternalCommit( + TestSubConversationDetails.groupId, + TestGroupInfo, + Either.Left(CoreFailure.Unknown(null)) + ) + } + + val result = staleEpochHandler.verifyEpoch(CONVERSATION_ID, subConversationId, null) + + result.shouldFail() + + coVerify { + arrangement.subconversationRepository.fetchRemoteSubConversationDetails(CONVERSATION_ID, subConversationId) + }.wasInvoked(once) + + coVerify { + arrangement.mlsConversationRepository.joinGroupByExternalCommit(TestSubConversationDetails.groupId, TestGroupInfo) + }.wasInvoked(once) + } + + @Test + fun givenSubconversationId_WhenVerified_ThenShouldNotCallFetchConversation() = runTest { + val subConversationId = SubconversationId("subconversation-id") + val (arrangement, staleEpochHandler) = arrange { + withFetchRemoteSubConversationDetails(CONVERSATION_ID, subConversationId, Either.Right(TestSubConversationDetails)) + withIsGroupOutOfSync(Either.Right(false)) + } + + val result = staleEpochHandler.verifyEpoch(CONVERSATION_ID, subConversationId, null) + + result.shouldSucceed() + + coVerify { + arrangement.subconversationRepository.fetchRemoteSubConversationDetails(CONVERSATION_ID, subConversationId) + }.wasInvoked(once) + + coVerify { + arrangement.conversationRepository.fetchConversation(any()) + }.wasNotInvoked() + } + private class Arrangement(private val block: suspend Arrangement.() -> Unit) : SystemMessageInserterArrangement by SystemMessageInserterArrangementImpl(), ConversationRepositoryArrangement by ConversationRepositoryArrangementImpl(), MLSConversationRepositoryArrangement by MLSConversationRepositoryArrangementImpl(), - JoinExistingMLSConversationUseCaseArrangement by JoinExistingMLSConversationUseCaseArrangementImpl() - { + SubconversationRepositoryArrangement by SubconversationRepositoryArrangementImpl(), + JoinExistingMLSConversationUseCaseArrangement by JoinExistingMLSConversationUseCaseArrangementImpl() { suspend fun arrange() = run { block() this@Arrangement to StaleEpochVerifierImpl( systemMessageInserter = systemMessageInserter, conversationRepository = conversationRepository, + subconversationRepository = subconversationRepository, mlsConversationRepository = mlsConversationRepository, joinExistingMLSConversation = joinExistingMLSConversationUseCase ) @@ -154,6 +277,18 @@ class StaleEpochVerifierTest { private companion object { suspend fun arrange(configuration: suspend Arrangement.() -> Unit) = Arrangement(configuration).arrange() - val CONVERSATION_ID = TestConversation.ID + val CONVERSATION_ID = ConversationId("conversation-value", "conversation-domain") + + val TestSubConversationDetails = SubConversation( + id = SubconversationId("subconversation-value"), + parentId = CONVERSATION_ID, + groupId = GroupID("sub-group-id"), + epoch = 123UL, + epochTimestamp = null, + mlsCipherSuiteTag = null, + members = emptyList() + ) + + val TestGroupInfo = ByteArray(0) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt index cccfce2047d..d503b658eb9 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt @@ -372,7 +372,11 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewMLSMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.staleEpochVerifier.verifyEpoch(eq(newMessageEvent.conversationId), eq(newMessageEvent.messageInstant)) + arrangement.staleEpochVerifier.verifyEpoch( + eq(newMessageEvent.conversationId), + any(), + eq(newMessageEvent.messageInstant) + ) }.wasInvoked(exactly = once) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/MLSConversationRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/MLSConversationRepositoryArrangement.kt index 801b5eb7127..0adae68fb38 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/MLSConversationRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/MLSConversationRepositoryArrangement.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.util.arrangement.mls import com.wire.kalium.cryptography.WireIdentity import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.MLSConversationRepository +import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.functional.Either import io.mockative.any @@ -32,6 +33,11 @@ interface MLSConversationRepositoryArrangement { suspend fun withIsGroupOutOfSync(result: Either) suspend fun withUserIdentity(result: Either>) suspend fun withMembersIdentities(result: Either>>) + suspend fun withJoinGroupByExternalCommit( + groupId: GroupID, + groupInfo: ByteArray, + result: Either + ): MLSConversationRepositoryArrangementImpl } class MLSConversationRepositoryArrangementImpl : MLSConversationRepositoryArrangement { @@ -54,4 +60,10 @@ class MLSConversationRepositoryArrangementImpl : MLSConversationRepositoryArrang mlsConversationRepository.getMembersIdentities(any(), any()) }.returns(result) } + + override suspend fun withJoinGroupByExternalCommit(groupId: GroupID, groupInfo: ByteArray, result: Either) = apply { + coEvery { + mlsConversationRepository.joinGroupByExternalCommit(groupId, groupInfo) + }.returns(result) + } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/SubconversationRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/SubconversationRepositoryArrangement.kt new file mode 100644 index 00000000000..ca118ab13c3 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/SubconversationRepositoryArrangement.kt @@ -0,0 +1,137 @@ +/* + * Wire + * Copyright (C) 2025 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.repository + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.conversation.SubConversation +import com.wire.kalium.logic.data.conversation.SubconversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.GroupID +import com.wire.kalium.logic.data.id.SubconversationId +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.mock + +internal interface SubconversationRepositoryArrangement { + val subconversationRepository: SubconversationRepository + + suspend fun withInsertSubconversation( + conversationId: ConversationId, + subConversationId: SubconversationId, + groupId: GroupID + ) + + suspend fun withGetSubconversationInfo( + conversationId: ConversationId, + subConversationId: SubconversationId, + result: GroupID? + ) + + suspend fun withContainsSubconversation(groupId: GroupID, result: Boolean) + + suspend fun withDeleteSubconversation(conversationId: ConversationId, subConversationId: SubconversationId) + + suspend fun withDeleteRemoteSubConversation( + conversationId: ConversationId, + subConversationId: SubconversationId, + result: Either + ) + + suspend fun withFetchRemoteSubConversationGroupInfo( + conversationId: ConversationId, + subConversationId: SubconversationId, + result: Either + ) + + suspend fun withFetchRemoteSubConversationDetails( + conversationId: ConversationId, + subConversationId: SubconversationId, + result: Either + ) +} + +internal class SubconversationRepositoryArrangementImpl : SubconversationRepositoryArrangement { + + @Mock + override val subconversationRepository: SubconversationRepository = mock(SubconversationRepository::class) + + override suspend fun withInsertSubconversation( + conversationId: ConversationId, + subConversationId: SubconversationId, + groupId: GroupID + ) { + coEvery { + subconversationRepository.insertSubconversation(conversationId, subConversationId, groupId) + }.returns(Unit) + } + + override suspend fun withGetSubconversationInfo( + conversationId: ConversationId, + subConversationId: SubconversationId, + result: GroupID? + ) { + coEvery { + subconversationRepository.getSubconversationInfo(conversationId, subConversationId) + }.returns(result) + } + + override suspend fun withContainsSubconversation(groupId: GroupID, result: Boolean) { + coEvery { + subconversationRepository.containsSubconversation(groupId) + }.returns(result) + } + + override suspend fun withDeleteSubconversation(conversationId: ConversationId, subConversationId: SubconversationId) { + coEvery { + subconversationRepository.deleteSubconversation(conversationId, subConversationId) + }.returns(Unit) + } + + override suspend fun withDeleteRemoteSubConversation( + conversationId: ConversationId, + subConversationId: SubconversationId, + result: Either + ) { + coEvery { + subconversationRepository.deleteRemoteSubConversation(conversationId, subConversationId, any()) + }.returns(result) + } + + override suspend fun withFetchRemoteSubConversationGroupInfo( + conversationId: ConversationId, + subConversationId: SubconversationId, + result: Either + ) { + coEvery { + subconversationRepository.fetchRemoteSubConversationGroupInfo(conversationId, subConversationId) + }.returns(result) + } + + override suspend fun withFetchRemoteSubConversationDetails( + conversationId: ConversationId, + subConversationId: SubconversationId, + result: Either + ) { + coEvery { + subconversationRepository.fetchRemoteSubConversationDetails(conversationId, subConversationId) + }.returns(result) + } +} diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt index 1347c7f1110..b496883c600 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt @@ -255,11 +255,11 @@ data class SubconversationResponse( val mlsCipherSuiteTag: Int?, @SerialName("members") - val members: List, + val members: List, ) @Serializable -data class SubconversationMember( +data class SubconversationMemberDTO( @SerialName("client_id") val clientId: String, @SerialName("user_id") val userId: String, @SerialName("domain") val domain: String From 9cdef1554cebd034e56a5859cae09390aeda0c5f Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 24 Jan 2025 12:24:27 +0100 Subject: [PATCH 2/4] test fix --- .../com/wire/kalium/logic/feature/message/MessageSenderTest.kt | 2 +- .../logic/util/arrangement/mls/StaleEpochVerifierArrangement.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageSenderTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageSenderTest.kt index 2a301d8afa0..845e7d2f917 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageSenderTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageSenderTest.kt @@ -323,7 +323,7 @@ class MessageSenderTest { // then result.shouldSucceed() coVerify { - arrangement.staleEpochVerifier.verifyEpoch(eq(Arrangement.TEST_CONVERSATION_ID), any()) + arrangement.staleEpochVerifier.verifyEpoch(eq(Arrangement.TEST_CONVERSATION_ID), any(), any()) }.wasInvoked(once) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/StaleEpochVerifierArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/StaleEpochVerifierArrangement.kt index 65f65d8f16b..a22c258b5a5 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/StaleEpochVerifierArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/mls/StaleEpochVerifierArrangement.kt @@ -40,7 +40,7 @@ class StaleEpochVerifierArrangementImpl : StaleEpochVerifierArrangement { override suspend fun withVerifyEpoch(result: Either) { coEvery { - staleEpochVerifier.verifyEpoch(any(), any()) + staleEpochVerifier.verifyEpoch(any(), any(), any()) }.returns(result) } } From 2fe18b752cda38bf0c679142f31587d482c088c5 Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 24 Jan 2025 13:29:26 +0100 Subject: [PATCH 3/4] detekt fix --- .../com/wire/kalium/logic/feature/message/MessageSenderTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageSenderTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageSenderTest.kt index 845e7d2f917..f1a10372135 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageSenderTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageSenderTest.kt @@ -64,7 +64,6 @@ import com.wire.kalium.logic.util.thenReturnSequentially import com.wire.kalium.network.api.base.authenticated.message.MLSMessageApi import com.wire.kalium.network.api.model.ErrorResponse import com.wire.kalium.network.exceptions.KaliumException -import com.wire.kalium.util.DateTimeUtil import com.wire.kalium.util.time.UNIX_FIRST_DATE import io.ktor.utils.io.core.toByteArray import io.mockative.Mock From bdec0691b980d8722a26535269c0f3d0cf8faf7f Mon Sep 17 00:00:00 2001 From: Jakub Zerko Date: Fri, 24 Jan 2025 14:05:56 +0100 Subject: [PATCH 4/4] test fix --- .../receiver/conversation/message/NewMessageEventHandlerTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt index aa24e47e219..6c5b3b6cbfc 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt @@ -493,7 +493,7 @@ class NewMessageEventHandlerTest { suspend fun withVerifyEpoch(result: Either) = apply { coEvery { - staleEpochVerifier.verifyEpoch(any(), any()) + staleEpochVerifier.verifyEpoch(any(), any(), any()) }.returns(result) }