diff --git a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/MLSClientImpl.kt b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/MLSClientImpl.kt index 32ce02ba02c..bfba4296917 100644 --- a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/MLSClientImpl.kt +++ b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/MLSClientImpl.kt @@ -327,9 +327,16 @@ class MLSClientImpl( value.handle, value.displayName, value.domain, - value.certificate + value.certificate, + toDeviceStatus(value.status) ) + fun toDeviceStatus(value: com.wire.crypto.DeviceStatus) = when (value) { + com.wire.crypto.DeviceStatus.VALID -> CryptoCertificateStatus.VALID + com.wire.crypto.DeviceStatus.EXPIRED -> CryptoCertificateStatus.EXPIRED + com.wire.crypto.DeviceStatus.REVOKED -> CryptoCertificateStatus.REVOKED + } + // TODO: remove later, when CoreCrypto return the groupId instead of Hex value @Suppress("MagicNumber") fun toGroupId(hexValue: String): MLSGroupId { @@ -366,9 +373,7 @@ class MLSClientImpl( value.commitDelay?.toLong(), value.senderClientId?.let { CryptoQualifiedClientId.fromEncodedString(String(it)) }, value.hasEpochChanged, - value.identity?.let { - WireIdentity(it.clientId, it.handle, it.displayName, it.domain, it.certificate) - } + value.identity?.let { toIdentity(it) } ) fun toDecryptedMessageBundle(value: BufferedDecryptedMessage) = DecryptedMessageBundle( @@ -376,9 +381,7 @@ class MLSClientImpl( value.commitDelay?.toLong(), value.senderClientId?.let { CryptoQualifiedClientId.fromEncodedString(String(it)) }, value.hasEpochChanged, - value.identity?.let { - WireIdentity(it.clientId, it.handle, it.displayName, it.domain, it.certificate) - } + value.identity?.let { toIdentity(it) } ) } } diff --git a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/IDs.kt b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/IDs.kt index d6d1aaf32ac..9f0ccb1aae9 100644 --- a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/IDs.kt +++ b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/IDs.kt @@ -78,9 +78,14 @@ data class WireIdentity( val handle: String, val displayName: String, val domain: String, - val certificate: String + val certificate: String, + val status: CryptoCertificateStatus ) +enum class CryptoCertificateStatus { + VALID, EXPIRED, REVOKED; +} + @Suppress("MagicNumber") data class E2EIQualifiedClientId( val value: String, diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt index dca61f435c1..57ded797c59 100644 --- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt +++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt @@ -17,12 +17,14 @@ */ package com.wire.kalium.logic.feature.e2ei +import com.wire.kalium.cryptography.CryptoCertificateStatus + actual interface CertificateStatusChecker { - actual fun status(notAfterTimestamp: Long): CertificateStatus + actual fun status(notAfterTimestamp: Long, certificateStatus: CryptoCertificateStatus): CertificateStatus } actual class CertificateStatusCheckerImpl : CertificateStatusChecker { - override fun status(notAfterTimestamp: Long): CertificateStatus { + override fun status(notAfterTimestamp: Long, certificateStatus: CryptoCertificateStatus): CertificateStatus { TODO("Not yet implemented") } } diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt index 321f41490e6..4b925cd1e5d 100644 --- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt +++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt @@ -17,15 +17,17 @@ */ package com.wire.kalium.logic.feature.e2ei +import com.wire.kalium.cryptography.CryptoCertificateStatus + actual interface PemCertificateDecoder { - actual fun decode(certificate: String): E2eiCertificate + actual fun decode(certificate: String, status: CryptoCertificateStatus): E2eiCertificate } actual class PemCertificateDecoderImpl actual constructor( private val x509CertificateGenerator: X509CertificateGenerator, private val certificateStatusChecker: CertificateStatusChecker ) : PemCertificateDecoder { - override fun decode(certificate: String): E2eiCertificate { + override fun decode(certificate: String, status: CryptoCertificateStatus): E2eiCertificate { TODO("Not yet implemented") } } diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt index 661bc3372ad..51d7991ca03 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt @@ -17,20 +17,21 @@ */ package com.wire.kalium.logic.feature.e2ei +import com.wire.kalium.cryptography.CryptoCertificateStatus import java.util.Date actual interface CertificateStatusChecker { - actual fun status(notAfterTimestamp: Long): CertificateStatus + actual fun status(notAfterTimestamp: Long, certificateStatus: CryptoCertificateStatus): CertificateStatus } actual class CertificateStatusCheckerImpl : CertificateStatusChecker { - override fun status(notAfterTimestamp: Long): CertificateStatus { - // TODO check for revoked from coreCrypto when API is ready - + override fun status(notAfterTimestamp: Long, certificateStatus: CryptoCertificateStatus): CertificateStatus { val current = Date() - println("current timestap is ${current.time}") - if (current.time >= notAfterTimestamp) - return CertificateStatus.EXPIRED - return CertificateStatus.VALID + + return when { + (certificateStatus == CryptoCertificateStatus.REVOKED) -> CertificateStatus.REVOKED + (current.time >= notAfterTimestamp || certificateStatus == CryptoCertificateStatus.EXPIRED) -> CertificateStatus.EXPIRED + else -> CertificateStatus.VALID + } } } diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt index 25fcbfc38d7..9e28ac4ebb4 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt @@ -17,21 +17,22 @@ */ package com.wire.kalium.logic.feature.e2ei +import com.wire.kalium.cryptography.CryptoCertificateStatus import com.wire.kalium.logic.util.serialNumber actual interface PemCertificateDecoder { - actual fun decode(certificate: String): E2eiCertificate + actual fun decode(certificate: String, status: CryptoCertificateStatus): E2eiCertificate } actual class PemCertificateDecoderImpl actual constructor( private val x509CertificateGenerator: X509CertificateGenerator, private val certificateStatusChecker: CertificateStatusChecker ) : PemCertificateDecoder { - override fun decode(certificate: String): E2eiCertificate { + override fun decode(certificate: String, status: CryptoCertificateStatus): E2eiCertificate { x509CertificateGenerator.generate(certificate.toByteArray()).also { return E2eiCertificate( issuer = it.value.issuerX500Principal.name, - status = certificateStatusChecker.status(it.value.notAfter.time), + status = certificateStatusChecker.status(it.value.notAfter.time, status), serialNumber = it.value.serialNumber.toString(BASE_16).serialNumber(), certificateDetail = certificate ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt index 5fd732083ca..af85e7fc73c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt @@ -215,9 +215,6 @@ interface ConversationRepository { suspend fun clearContent(conversationId: ConversationId): Either suspend fun observeIsUserMember(conversationId: ConversationId, userId: UserId): Flow> suspend fun whoDeletedMe(conversationId: ConversationId): Either - - suspend fun deleteUserFromConversations(userId: UserId): Either - suspend fun getConversationsByUserId(userId: UserId): Either> suspend fun insertConversations(conversations: List): Either suspend fun changeConversationName( @@ -875,10 +872,6 @@ internal class ConversationDataSource internal constructor( )?.toModel() } - override suspend fun deleteUserFromConversations(userId: UserId): Either = wrapStorageRequest { - conversationDAO.revokeOneOnOneConversationsWithDeletedUser(userId.toDao()) - } - override suspend fun getConversationsByUserId(userId: UserId): Either> { return wrapStorageRequest { conversationDAO.getConversationsByUserId(userId.toDao()) } .map { it.map { entity -> conversationMapper.fromDaoModel(entity) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/team/TeamRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/team/TeamRepository.kt index a75644216b8..8f2f022ad32 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/team/TeamRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/team/TeamRepository.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap 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.sync.receiver.handler.legalhold.LegalHoldHandler import com.wire.kalium.logic.sync.receiver.handler.legalhold.LegalHoldRequestHandler @@ -50,6 +51,7 @@ import kotlinx.coroutines.flow.map interface TeamRepository { suspend fun fetchTeamById(teamId: TeamId): Either + suspend fun fetchMembersByTeamId(teamId: TeamId, userDomain: String): Either suspend fun getTeam(teamId: TeamId): Flow suspend fun deleteConversation(conversationId: ConversationId, teamId: TeamId): Either suspend fun updateMemberRole(teamId: String, userId: String, permissionCode: Int?): Either @@ -81,11 +83,45 @@ internal class TeamDataSource( }.map { teamDTO -> teamMapper.fromDtoToEntity(teamDTO) }.flatMap { teamEntity -> - wrapStorageRequest { teamDAO.insertTeam(team = teamEntity) }.map { + wrapStorageRequest { + teamDAO.insertTeam(teamEntity) + }.map { teamMapper.fromDaoModelToTeam(teamEntity) } } + override suspend fun fetchMembersByTeamId(teamId: TeamId, userDomain: String): Either { + var hasMore = true + var error: CoreFailure? = null + while (hasMore && error == null) { + wrapApiRequest { + teamsApi.getTeamMembers( + teamId = teamId.value, + limitTo = FETCH_TEAM_MEMBER_PAGE_SIZE + ) + }.onSuccess { + hasMore = it.hasMore + }.map { + it.members.map { teamMember -> + val userId = QualifiedIDEntity(teamMember.nonQualifiedUserId, userDomain) + val userType = userTypeEntityTypeMapper.teamRoleCodeToUserType(teamMember.permissions?.own) + userId to userType + } + }.flatMap { teamMembers -> + wrapStorageRequest { + userDAO.upsertTeamMemberUserTypes(teamMembers.toMap()) + } + }.onFailure { + error = it + } + } + return if (error != null) { + Either.Left(error!!) + } else { + Either.Right(Unit) + } + } + override suspend fun getTeam(teamId: TeamId): Flow = teamDAO.getTeamById(teamId.value) .map { @@ -147,6 +183,7 @@ internal class TeamDataSource( eventContentDTO = EventContentDTO.User.LegalHoldEnabledDTO(id = selfUserId.toString()) ) ) + LegalHoldStatusDTO.DISABLED -> legalHoldHandler.handleDisable( eventMapper.legalHoldDisabled( id = LocalId.generate(), @@ -155,6 +192,7 @@ internal class TeamDataSource( eventContentDTO = EventContentDTO.User.LegalHoldDisabledDTO(id = selfUserId.toString()) ) ) + LegalHoldStatusDTO.PENDING -> legalHoldRequestHandler.handle( eventMapper.legalHoldRequest( @@ -168,7 +206,12 @@ internal class TeamDataSource( ) ) ) + LegalHoldStatusDTO.NO_CONSENT -> Either.Right(Unit) }.map { legalHoldStatusMapper.fromApiModel(response.legalHoldStatusDTO) } } + + private companion object { + const val FETCH_TEAM_MEMBER_PAGE_SIZE = 200 + } } 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 23ebf03f50b..4558325f7cb 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 @@ -110,7 +110,7 @@ interface UserRepository { */ suspend fun getAllRecipients(): Either, List>> suspend fun updateUserFromEvent(event: Event.User.Update): Either - suspend fun removeUser(userId: UserId): Either + suspend fun markUserAsDeletedAndRemoveFromGroupConversations(userId: UserId): Either /** * Marks federated user as defederated in order to hold conversation history @@ -502,9 +502,9 @@ internal class UserDataSource internal constructor( } } - override suspend fun removeUser(userId: UserId): Either { + override suspend fun markUserAsDeletedAndRemoveFromGroupConversations(userId: UserId): Either { return wrapStorageRequest { - userDAO.markUserAsDeleted(userId.toDao()) + userDAO.markUserAsDeletedAndRemoveFromGroupConv(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 bf4e2de904c..7bfa72767c4 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 @@ -875,7 +875,9 @@ class UserSessionScope internal constructor( private val syncSelfTeamUseCase: SyncSelfTeamUseCase get() = SyncSelfTeamUseCaseImpl( - userRepository = userRepository, teamRepository = teamRepository + userRepository = userRepository, + teamRepository = teamRepository, + fetchAllTeamMembersEagerly = kaliumConfigs.fetchAllTeamMembersEagerly ) private val joinExistingMLSConversationUseCase: JoinExistingMLSConversationUseCase @@ -1088,22 +1090,23 @@ class UserSessionScope internal constructor( ) }) - internal val mlsMigrationWorker get() = - MLSMigrationWorkerImpl( - userConfigRepository, - featureConfigRepository, - mlsConfigHandler, - mlsMigrationConfigHandler, - mlsMigrator, - ) + internal val mlsMigrationWorker + get() = + MLSMigrationWorkerImpl( + userConfigRepository, + featureConfigRepository, + mlsConfigHandler, + mlsMigrationConfigHandler, + mlsMigrator, + ) internal val mlsMigrationManager: MLSMigrationManager = MLSMigrationManagerImpl( - kaliumConfigs, - featureSupport, - incrementalSyncRepository, - lazy { clientRepository }, - lazy { users.timestampKeyRepository }, - lazy { mlsMigrationWorker } + kaliumConfigs, + featureSupport, + incrementalSyncRepository, + lazy { clientRepository }, + lazy { users.timestampKeyRepository }, + lazy { mlsMigrationWorker } ) private val mlsPublicKeysRepository: MLSPublicKeysRepository diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt index 3650b4d04d8..b326a8f5177 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusChecker.kt @@ -17,8 +17,10 @@ */ package com.wire.kalium.logic.feature.e2ei +import com.wire.kalium.cryptography.CryptoCertificateStatus + expect interface CertificateStatusChecker { - fun status(notAfterTimestamp: Long): CertificateStatus + fun status(notAfterTimestamp: Long, certificateStatus: CryptoCertificateStatus): CertificateStatus } expect class CertificateStatusCheckerImpl() : CertificateStatusChecker diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt index fe46b545584..3dc68939ae4 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoder.kt @@ -17,8 +17,10 @@ */ package com.wire.kalium.logic.feature.e2ei +import com.wire.kalium.cryptography.CryptoCertificateStatus + expect interface PemCertificateDecoder { - fun decode(certificate: String): E2eiCertificate + fun decode(certificate: String, status: CryptoCertificateStatus): E2eiCertificate } expect class PemCertificateDecoderImpl( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetE2EICertificateUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetE2EICertificateUseCase.kt index 9957e1243ff..561e880c446 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetE2EICertificateUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetE2EICertificateUseCase.kt @@ -40,7 +40,7 @@ class GetE2eiCertificateUseCaseImpl internal constructor( GetE2EICertificateUseCaseResult.Failure.NotActivated }, { - val certificate = pemCertificateDecoder.decode(it.certificate) + val certificate = pemCertificateDecoder.decode(it.certificate, it.status) GetE2EICertificateUseCaseResult.Success(certificate) } ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/team/SyncSelfTeamUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/team/SyncSelfTeamUseCase.kt index be5d8003135..ea665c53615 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/team/SyncSelfTeamUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/team/SyncSelfTeamUseCase.kt @@ -23,7 +23,7 @@ import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.team.TeamRepository import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.onSuccess import com.wire.kalium.logic.kaliumLogger import kotlinx.coroutines.flow.first @@ -34,18 +34,27 @@ internal interface SyncSelfTeamUseCase { internal class SyncSelfTeamUseCaseImpl( private val userRepository: UserRepository, - private val teamRepository: TeamRepository + private val teamRepository: TeamRepository, + private val fetchAllTeamMembersEagerly: Boolean ) : SyncSelfTeamUseCase { override suspend fun invoke(): Either { val user = userRepository.observeSelfUser().first() return user.teamId?.let { teamId -> - teamRepository.fetchTeamById(teamId = teamId) - .map { } - .onSuccess { - teamRepository.syncServices(teamId = teamId) + teamRepository.fetchTeamById(teamId = teamId).flatMap { + if (fetchAllTeamMembersEagerly) { + kaliumLogger.withFeatureId(SYNC).i("Fetching all team members eagerly") + teamRepository.fetchMembersByTeamId( + teamId = teamId, + userDomain = user.id.domain + ) + } else { + Either.Right(Unit) } + }.onSuccess { + teamRepository.syncServices(teamId = teamId) + } } ?: run { kaliumLogger.withFeatureId(SYNC).i("Skipping team sync because user doesn't belong to a team") Either.Right(Unit) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/KaliumConfigs.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/KaliumConfigs.kt index 4aea8d10954..9bb7ae67576 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/KaliumConfigs.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/KaliumConfigs.kt @@ -44,7 +44,8 @@ data class KaliumConfigs( val kaliumMockEngine: KaliumMockEngine? = null, val mockNetworkStateObserver: NetworkStateObserver? = null, // Interval between attempts to advance the proteus to MLS migration - val mlsMigrationInterval: Duration = 24.hours + val mlsMigrationInterval: Duration = 24.hours, + val fetchAllTeamMembersEagerly: Boolean = false, ) sealed interface BuildFileRestrictionState { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserEventReceiver.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserEventReceiver.kt index fa016d0e897..6d9955eb80f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserEventReceiver.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserEventReceiver.kt @@ -182,28 +182,7 @@ internal class UserEventReceiverImpl internal constructor( logout(LogoutReason.DELETED_ACCOUNT) Either.Right(Unit) } else { - // TODO: those 2 steps must be done in one transaction - // userRepo.markAsDeleted(event.userId) will mark user as deleted and remove from the group conversations - userRepository.removeUser(event.userId) - .onSuccess { - conversationRepository.deleteUserFromConversations(event.userId) - .onSuccess { - kaliumLogger - .logEventProcessing( - EventLoggingStatus.SUCCESS, - event - ) - } - .onFailure { - kaliumLogger - .logEventProcessing( - EventLoggingStatus.FAILURE, - event, - Pair("errorInfo", "$it") - ) - } - - } + userRepository.markUserAsDeletedAndRemoveFromGroupConversations(event.userId) .onFailure { kaliumLogger .logEventProcessing( 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 5b9c228dfec..afe45503a15 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 @@ -20,6 +20,7 @@ package com.wire.kalium.logic.data.conversation import com.benasher44.uuid.uuid4 import com.wire.kalium.cryptography.CommitBundle +import com.wire.kalium.cryptography.CryptoCertificateStatus import com.wire.kalium.cryptography.E2EIClient import com.wire.kalium.cryptography.E2EIConversationState import com.wire.kalium.cryptography.GroupInfoBundle @@ -1616,7 +1617,7 @@ class MLSConversationRepositoryTest { ) val COMMIT_BUNDLE = CommitBundle(COMMIT, WELCOME, PUBLIC_GROUP_STATE_BUNDLE) val ROTATE_BUNDLE = RotateBundle(mapOf(RAW_GROUP_ID to COMMIT_BUNDLE), emptyList(), emptyList()) - val WIRE_IDENTITY = WireIdentity("id", "user_handle", "User Test", "domain.com", "certificate") + val WIRE_IDENTITY = WireIdentity("id", "user_handle", "User Test", "domain.com", "certificate", CryptoCertificateStatus.VALID) val E2EI_CONVERSATION_CLIENT_INFO_ENTITY = E2EIConversationClientInfoEntity(UserIDEntity(uuid4().toString(), "domain.com"), "clientId", "groupId") val DECRYPTED_MESSAGE_BUNDLE = com.wire.kalium.cryptography.DecryptedMessageBundle( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/team/TeamRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/team/TeamRepositoryTest.kt index 038bafc19bf..566295e6efc 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/team/TeamRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/team/TeamRepositoryTest.kt @@ -18,7 +18,7 @@ package com.wire.kalium.logic.data.team -import app.cash.turbine.test +import app.cash.turbine.test import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.user.LegalHoldStatus import com.wire.kalium.logic.data.id.TeamId @@ -34,7 +34,6 @@ import com.wire.kalium.logic.util.shouldSucceed import com.wire.kalium.network.api.base.authenticated.TeamsApi import com.wire.kalium.network.api.base.authenticated.client.ClientIdDTO import com.wire.kalium.network.api.base.authenticated.keypackage.LastPreKeyDTO -import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi import com.wire.kalium.network.api.base.model.ErrorResponse import com.wire.kalium.network.api.base.model.LegalHoldStatusDTO import com.wire.kalium.network.api.base.model.LegalHoldStatusResponse @@ -50,6 +49,7 @@ import com.wire.kalium.persistence.dao.UserDAO import com.wire.kalium.persistence.dao.unread.UserConfigDAO import io.mockative.Mock import io.mockative.any +import io.mockative.anything import io.mockative.classOf import io.mockative.configure import io.mockative.eq @@ -59,11 +59,13 @@ import io.mockative.mock import io.mockative.once import io.mockative.oneOf import io.mockative.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals +@OptIn(ExperimentalCoroutinesApi::class) class TeamRepositoryTest { @Test fun givenSelfUserExists_whenFetchingTeamInfo_thenTeamInfoShouldBeSuccessful() = runTest { @@ -104,6 +106,52 @@ class TeamRepositoryTest { } } + @Test + fun givenTeamIdAndUserDomain_whenFetchingTeamMembers_thenTeamMembersShouldBeSuccessful() = runTest { + val teamMember = TestTeam.memberDTO( + nonQualifiedUserId = "teamMember1" + ) + + val teamMembersList = TeamsApi.TeamMemberList( + hasMore = false, + members = listOf( + teamMember + ) + ) + + val (arrangement, teamRepository) = Arrangement() + .withGetTeamMembers(NetworkResponse.Success(teamMembersList, mapOf(), 200)) + .arrange() + + val result = teamRepository.fetchMembersByTeamId(teamId = TeamId("teamId"), userDomain = "userDomain") + + // Verifies that userDAO insertUsers was called with the correct mapped values + verify(arrangement.userDAO) + .suspendFunction(arrangement.userDAO::upsertTeamMemberUserTypes) + .with(any()) + .wasInvoked(exactly = once) + + // Verifies that when fetching members by team id, it succeeded + result.shouldSucceed() + } + + @Test + fun givenTeamApiFails_whenFetchingTeamMembers_thenTheFailureIsPropagated() = runTest { + val (arrangement, teamRepository) = Arrangement() + .arrange() + + given(arrangement.teamsApi) + .suspendFunction(arrangement.teamsApi::getTeamMembers) + .whenInvokedWith(any(), anything()) + .thenReturn(NetworkResponse.Error(KaliumException.ServerError(ErrorResponse(500, "error_message", "error_label")))) + + val result = teamRepository.fetchMembersByTeamId(teamId = TeamId("teamId"), userDomain = "userDomain") + + result.shouldFail { + assertEquals(it::class, NetworkFailure.ServerMiscommunication::class) + } + } + @Test fun givenSelfUserExists_whenGettingTeamById_thenTeamDataShouldBePassed() = runTest { val teamEntity = TeamEntity(id = "teamId", name = "teamName", icon = "icon") @@ -324,13 +372,13 @@ class TeamRepositoryTest { stubsUnitByDefault = true } + val teamMapper = MapperProvider.teamMapper() + @Mock val userConfigDAO = configure(mock(classOf())) { stubsUnitByDefault = true } - val teamMapper = MapperProvider.teamMapper() - @Mock val teamsApi = mock(classOf()) @@ -350,11 +398,12 @@ class TeamRepositoryTest { teamMapper = teamMapper, teamsApi = teamsApi, userDAO = userDAO, - userConfigDAO = userConfigDAO, selfUserId = TestUser.USER_ID, serviceDAO = serviceDAO, legalHoldHandler = legalHoldHandler, legalHoldRequestHandler = legalHoldRequestHandler, + userConfigDAO = userConfigDAO + ) fun withApiGetTeamInfoSuccess(teamDTO: TeamDTO) = apply { @@ -364,13 +413,6 @@ class TeamRepositoryTest { .then { NetworkResponse.Success(value = teamDTO, headers = mapOf(), httpCode = 200) } } - fun withApiGetTeamMemberSuccess(teamMemberDTO: TeamsApi.TeamMemberDTO) = apply { - given(teamsApi) - .suspendFunction(teamsApi::getTeamMember) - .whenInvokedWith(any(), any()) - .thenReturn(NetworkResponse.Success(value = teamMemberDTO, headers = mapOf(), httpCode = 200)) - } - fun withFetchWhiteListedServicesSuccess() = apply { given(teamsApi) .suspendFunction(teamsApi::whiteListedServices) @@ -407,6 +449,14 @@ class TeamRepositoryTest { .thenReturn(Either.Right(Unit)) } + fun withGetTeamMembers(result: NetworkResponse) = apply { + given(teamsApi) + .suspendFunction(teamsApi::getTeamMembers) + .whenInvokedWith(any(), any()) + .thenReturn(result) + + } + fun arrange() = this to teamRepository companion object { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt index f987a81745f..a492c25283e 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt @@ -37,7 +37,6 @@ import com.wire.kalium.logic.framework.TestUser.LIST_USERS_DTO import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.getOrNull import com.wire.kalium.logic.test_util.TestNetworkResponseError -import com.wire.kalium.logic.sync.receiver.UserEventReceiverTest import com.wire.kalium.logic.test_util.TestNetworkException.federationNotEnabled import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetE2eiCertificateUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetE2eiCertificateUseCaseTest.kt index d18ac5e8027..80ae133899c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetE2eiCertificateUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetE2eiCertificateUseCaseTest.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.logic.feature.e2ei +import com.wire.kalium.cryptography.CryptoCertificateStatus import com.wire.kalium.cryptography.WireIdentity import com.wire.kalium.logic.E2EIFailure import com.wire.kalium.logic.feature.e2ei.usecase.GetE2eiCertificateUseCaseImpl @@ -118,7 +119,8 @@ class GetE2eiCertificateUseCaseTest { handle = "alic_test", displayName = "Alice Test", domain = "test.com", - certificate = "certificate" + certificate = "certificate", + status = CryptoCertificateStatus.EXPIRED ) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/team/SyncSelfTeamUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/team/SyncSelfTeamUseCaseTest.kt index 2f8d1e7c436..89e6344bc0f 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/team/SyncSelfTeamUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/team/SyncSelfTeamUseCaseTest.kt @@ -38,9 +38,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import kotlin.properties.Delegates import kotlin.test.Test -@OptIn(ExperimentalCoroutinesApi::class) class SyncSelfTeamUseCaseTest { @Test @@ -54,6 +54,7 @@ class SyncSelfTeamUseCaseTest { val (arrangement, syncSelfTeamUseCase) = Arrangement() .withSelfUser(selfUserFlow) + .witFetchAllTeamMembersEagerly(false) .arrange() // when @@ -64,7 +65,10 @@ class SyncSelfTeamUseCaseTest { .suspendFunction(arrangement.teamRepository::fetchTeamById) .with(any()) .wasNotInvoked() - + verify(arrangement.teamRepository) + .suspendFunction(arrangement.teamRepository::fetchMembersByTeamId) + .with(any(), any()) + .wasNotInvoked() verify(arrangement.teamRepository) .suspendFunction(arrangement.teamRepository::syncServices) .with(any()) @@ -72,13 +76,15 @@ class SyncSelfTeamUseCaseTest { } @Test - fun givenSelfUserHasValidTeam_whenSyncingSelfTeam_thenTeamInfoAndServicesAreRequestedSuccessfully() = runTest { + fun givenSelfUserHasValidTeamAndFetchAllTeamMembersEagerlyIsTrue_whenSyncingSelfTeam_thenTeamInfoAndServicesAreRequestedSuccessfully() = runTest { // given val selfUserFlow = flowOf(TestUser.SELF) val (arrangement, syncSelfTeamUseCase) = Arrangement() .withSelfUser(selfUserFlow) + .witFetchAllTeamMembersEagerly(true) .withTeam() + .withTeamMembers() .withServicesSync() .arrange() @@ -90,7 +96,13 @@ class SyncSelfTeamUseCaseTest { .suspendFunction(arrangement.teamRepository::fetchTeamById) .with(eq(TestUser.SELF.teamId)) .wasInvoked(exactly = once) - + verify(arrangement.teamRepository) + .suspendFunction(arrangement.teamRepository::fetchMembersByTeamId) + .with( + eq(TestUser.SELF.teamId), + eq(TestUser.SELF.id.domain) + ) + .wasInvoked(exactly = once) verify(arrangement.teamRepository) .suspendFunction(arrangement.teamRepository::syncServices) .with(eq(TestUser.SELF.teamId)) @@ -104,6 +116,7 @@ class SyncSelfTeamUseCaseTest { val (arrangement, syncSelfTeamUseCase) = Arrangement() .withSelfUser(selfUserFlow) + .witFetchAllTeamMembersEagerly(false) .withFailingTeamInfo() .arrange() @@ -115,11 +128,20 @@ class SyncSelfTeamUseCaseTest { .suspendFunction(arrangement.teamRepository::fetchTeamById) .with(eq(TestUser.SELF.teamId)) .wasInvoked(exactly = once) - + verify(arrangement.teamRepository) + .suspendFunction(arrangement.teamRepository::fetchMembersByTeamId) + .with(any(), any()) + .wasNotInvoked() verify(arrangement.teamRepository) .suspendFunction(arrangement.teamRepository::syncServices) .with(any()) .wasNotInvoked() + verify(arrangement.teamRepository) + .suspendFunction(arrangement.teamRepository::fetchMembersByTeamId) + .with( + eq(TestUser.SELF.teamId), + eq(TestUser.SELF.id.domain) + ).wasNotInvoked() } @Test @@ -129,7 +151,9 @@ class SyncSelfTeamUseCaseTest { val (_, syncSelfTeamUseCase) = Arrangement() .withSelfUser(selfUserFlow) + .witFetchAllTeamMembersEagerly(false) .withTeam() + .withTeamMembers() .withFailingServicesSync() .arrange() @@ -140,18 +164,50 @@ class SyncSelfTeamUseCaseTest { result.shouldSucceed() } + @Test + fun givenSelfUserHasValidTeamAndFetchAllTeamMembersEagerlyIsFalse_whenSyncingSelfTeam_thenTeamInfoAndServicesAreRequestedSuccessfully() = runTest { + // given + val selfUserFlow = flowOf(TestUser.SELF) + + val (arrangement, syncSelfTeamUseCase) = Arrangement() + .withSelfUser(selfUserFlow) + .witFetchAllTeamMembersEagerly(false) + .withTeam() + .withServicesSync() + .arrange() + + // when + syncSelfTeamUseCase.invoke() + + // then + verify(arrangement.teamRepository) + .suspendFunction(arrangement.teamRepository::fetchTeamById) + .with(eq(TestUser.SELF.teamId)) + .wasInvoked(exactly = once) + verify(arrangement.teamRepository) + .suspendFunction(arrangement.teamRepository::fetchMembersByTeamId) + .with(any(), any()) + .wasNotInvoked() + verify(arrangement.teamRepository) + .suspendFunction(arrangement.teamRepository::syncServices) + .with(eq(TestUser.SELF.teamId)) + .wasInvoked(exactly = once) + } + private class Arrangement { + var fetchAllTeamMembersEagerly by Delegates.notNull() + @Mock val userRepository = mock(classOf()) @Mock val teamRepository = mock(classOf()) - val syncSelfTeamUseCase = SyncSelfTeamUseCaseImpl( - userRepository = userRepository, - teamRepository = teamRepository - ) + private lateinit var syncSelfTeamUseCase: SyncSelfTeamUseCase + fun witFetchAllTeamMembersEagerly(result: Boolean) = apply { + fetchAllTeamMembersEagerly = result + } fun withSelfUser(selfUserFlow: Flow) = apply { given(userRepository) @@ -174,6 +230,13 @@ class SyncSelfTeamUseCaseTest { .thenReturn(Either.Left(NetworkFailure.ServerMiscommunication(TestNetworkException.badRequest))) } + fun withTeamMembers() = apply { + given(teamRepository) + .suspendFunction(teamRepository::fetchMembersByTeamId) + .whenInvokedWith(any(), any()) + .thenReturn(Either.Right(Unit)) + } + fun withServicesSync() = apply { given(teamRepository) .suspendFunction(teamRepository::syncServices) @@ -188,10 +251,13 @@ class SyncSelfTeamUseCaseTest { .thenReturn(Either.Left(NetworkFailure.ServerMiscommunication(TestNetworkException.accessDenied))) } - fun arrange() = this to syncSelfTeamUseCase - - companion object { - + fun arrange(): Pair { + syncSelfTeamUseCase = SyncSelfTeamUseCaseImpl( + userRepository = userRepository, + teamRepository = teamRepository, + fetchAllTeamMembersEagerly + ) + return this to syncSelfTeamUseCase } } } 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 ace49053405..014c8b970cc 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 @@ -109,20 +109,14 @@ class UserEventReceiverTest { fun givenUserDeleteEvent_RepoAndPersisMessageAreInvoked() = runTest { val event = TestEvent.userDelete(userId = OTHER_USER_ID) val (arrangement, eventReceiver) = arrange { - withRemoveUserSuccess() - withDeleteUserFromConversationsSuccess() + withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess() withConversationsByUserId(listOf(TestConversation.CONVERSATION)) } eventReceiver.onEvent(event) verify(arrangement.userRepository) - .suspendFunction(arrangement.userRepository::removeUser) - .with(any()) - .wasInvoked(exactly = once) - - verify(arrangement.conversationRepository) - .suspendFunction(arrangement.conversationRepository::deleteUserFromConversations) + .suspendFunction(arrangement.userRepository::markUserAsDeletedAndRemoveFromGroupConversations) .with(any()) .wasInvoked(exactly = once) } @@ -329,11 +323,6 @@ class UserEventReceiverTest { given(logoutUseCase).suspendFunction(logoutUseCase::invoke).whenInvokedWith(any()).thenReturn(Unit) } - fun withDeleteUserFromConversationsSuccess() = apply { - given(conversationRepository).suspendFunction(conversationRepository::deleteUserFromConversations) - .whenInvokedWith(any()).thenReturn(Either.Right(Unit)) - } - fun withConversationsByUserId(conversationIds: List) = apply { given(conversationRepository).suspendFunction(conversationRepository::getConversationsByUserId) .whenInvokedWith(any()).thenReturn(Either.Right(conversationIds)) 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 index 2765bd50995..3f9d74dfc6f 100644 --- 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 @@ -37,7 +37,7 @@ internal interface UserRepositoryArrangement { fun withUpdateUserFailure(coreFailure: CoreFailure) - fun withRemoveUserSuccess() + fun withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess() fun withSelfUserReturning(selfUser: SelfUser?) @@ -74,8 +74,8 @@ internal class UserRepositoryArrangementImpl: UserRepositoryArrangement { .whenInvokedWith(any()).thenReturn(Either.Left(coreFailure)) } - override fun withRemoveUserSuccess() { - given(userRepository).suspendFunction(userRepository::removeUser) + override fun withMarkUserAsDeletedAndRemoveFromGroupConversationsSuccess() { + given(userRepository).suspendFunction(userRepository::markUserAsDeletedAndRemoveFromGroupConversations) .whenInvokedWith(any()).thenReturn(Either.Right(Unit)) } diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusCheckerTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusCheckerTest.kt index dcd33e61de9..ad3aaa79cb8 100644 --- a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusCheckerTest.kt +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateStatusCheckerTest.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.logic.feature.e2ei +import com.wire.kalium.cryptography.CryptoCertificateStatus import org.junit.Test import kotlin.test.assertEquals @@ -28,11 +29,33 @@ class CertificateStatusCheckerTest { val (_, certificateStatusChecker) = Arrangement() .arrange() - val result = certificateStatusChecker.status(timestamp) + val result = certificateStatusChecker.status(timestamp, CryptoCertificateStatus.VALID) assertEquals(CertificateStatus.EXPIRED, result) } + @Test + fun givenFutureTimestampAndExpiredCertificateStatus_whenCheckingTheStatus_thenReturnExpired() { + val timestamp = 4822355515000 // Sunday, 25 October 2122 07:11:55 + val (_, certificateStatusChecker) = Arrangement() + .arrange() + + val result = certificateStatusChecker.status(timestamp, CryptoCertificateStatus.EXPIRED) + + assertEquals(CertificateStatus.EXPIRED, result) + } + + @Test + fun givenFutureTimestampAndRevokedCertificateStatus_whenCheckingTheStatus_thenReturnExpired() { + val timestamp = 4822355515000 // Sunday, 25 October 2122 07:11:55 + val (_, certificateStatusChecker) = Arrangement() + .arrange() + + val result = certificateStatusChecker.status(timestamp, CryptoCertificateStatus.REVOKED) + + assertEquals(CertificateStatus.REVOKED, result) + } + @Test fun givenFutureTimestamp_whenCheckingTheStatus_thenReturnValid() { val timestamp = 4822355515000 // Sunday, 25 October 2122 07:11:55 @@ -40,7 +63,7 @@ class CertificateStatusCheckerTest { val (_, certificateStatusChecker) = Arrangement() .arrange() - val result = certificateStatusChecker.status(timestamp) + val result = certificateStatusChecker.status(timestamp, CryptoCertificateStatus.VALID) assertEquals(CertificateStatus.VALID, result) } @@ -49,4 +72,4 @@ class CertificateStatusCheckerTest { fun arrange() = this to CertificateStatusCheckerImpl() } -} \ No newline at end of file +} diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoderTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoderTest.kt index 1561b06e9c0..4b13e00b6f9 100644 --- a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoderTest.kt +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/e2ei/PemCertificateDecoderTest.kt @@ -17,6 +17,7 @@ */ package com.wire.kalium.logic.feature.e2ei +import com.wire.kalium.cryptography.CryptoCertificateStatus import io.mockative.Mock import io.mockative.any import io.mockative.classOf @@ -43,7 +44,7 @@ class PemCertificateDecoderTest { .withValidStatus() .arrange() - val result = pemCertificateDecoder.decode(validPemCertificateString) + val result = pemCertificateDecoder.decode(validPemCertificateString, CryptoCertificateStatus.VALID) verify(arrangement.x509CertificateGeneratorMock) .function(arrangement.x509CertificateGeneratorMock::generate) @@ -60,7 +61,7 @@ class PemCertificateDecoderTest { .withValidStatus() .arrange() - pemCertificateDecoder.decode(invalidPemCertificateString) + pemCertificateDecoder.decode(invalidPemCertificateString, CryptoCertificateStatus.VALID) } class Arrangement { diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt index 452d27dff97..f0c76cd4db1 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt @@ -35,6 +35,7 @@ fun coreLogic( encryptProteusStorage = true, isMLSSupportEnabled = true, wipeOnDeviceRemoval = true, + fetchAllTeamMembersEagerly = true, ), "Wire Infinite Monkeys" ) coreLogic.updateApiVersionsScheduler.scheduleImmediateApiVersionUpdate() diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Members.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Members.sq index d8b9d2b50f7..ccf2784e8b2 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Members.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Members.sq @@ -63,14 +63,6 @@ UPDATE Member SET role = ? WHERE user = ? AND conversation = ?; -deleteUserFromGroupConversations: -DELETE FROM Member -WHERE conversation IN ( - SELECT conversation FROM Member - JOIN Conversation ON Conversation.qualified_id = Member.conversation - WHERE Member.user = ? AND Conversation.type = 'GROUP' -) AND Member.user = ?; - isUserMember: SELECT user FROM Member WHERE conversation = :conversationId AND user = :userId; 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 b54af091006..392d8c0ec0a 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 @@ -82,9 +82,18 @@ connection_status = excluded.connection_status, user_type = excluded.user_type; markUserAsDeleted: -UPDATE User -SET team = NULL , preview_asset_id = NULL, complete_asset_id = NULL, user_type = ?, deleted = 1 -WHERE qualified_id = ?; +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; + +deleteUserFromGroupConversations: +DELETE FROM Member +WHERE conversation IN ( + SELECT conversation FROM Member + JOIN Conversation ON Conversation.qualified_id = Member.conversation + WHERE Member.user = :qualified_id AND Conversation.type = 'GROUP' +) AND Member.user = :qualified_id; markUserAsDefederated: UPDATE User 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 0d2228efe5d..7178236b6bb 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 @@ -266,7 +266,7 @@ interface UserDAO { suspend fun getUsersWithOneOnOneConversation(): List suspend fun deleteUserByQualifiedID(qualifiedID: QualifiedIDEntity) - suspend fun markUserAsDeleted(qualifiedID: QualifiedIDEntity) + suspend fun markUserAsDeletedAndRemoveFromGroupConv(qualifiedID: QualifiedIDEntity) suspend fun markUserAsDefederated(qualifiedID: QualifiedIDEntity) suspend fun updateUserHandle(qualifiedID: QualifiedIDEntity, handle: String) suspend fun updateUserAvailabilityStatus(qualifiedID: QualifiedIDEntity, status: UserAvailabilityStatusEntity) 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 4d346d2a036..7b9b15da622 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 @@ -208,25 +208,30 @@ class UserDAOImpl internal constructor( override suspend fun upsertUsers(users: List) = withContext(queriesContext) { userQueries.transaction { for (user: UserEntity in users) { - userQueries.insertUser( - qualified_id = user.id, - name = user.name, - handle = user.handle, - email = user.email, - phone = user.phone, - accent_id = user.accentId, - team = user.team, - preview_asset_id = user.previewAssetId, - complete_asset_id = user.completeAssetId, - user_type = user.userType, - bot_service = user.botService, - incomplete_metadata = user.hasIncompleteMetadata, - expires_at = user.expiresAt, - connection_status = user.connectionStatus, - deleted = user.deleted, - supported_protocols = user.supportedProtocols, - active_one_on_one_conversation_id = user.activeOneOnOneConversationId - ) + if (user.deleted) { + // mark as deleted and remove from groups + safeMarkAsDeleted(user.id) + } else { + userQueries.insertUser( + qualified_id = user.id, + name = user.name, + handle = user.handle, + email = user.email, + phone = user.phone, + accent_id = user.accentId, + team = user.team, + preview_asset_id = user.previewAssetId, + complete_asset_id = user.completeAssetId, + user_type = user.userType, + bot_service = user.botService, + incomplete_metadata = user.hasIncompleteMetadata, + expires_at = user.expiresAt, + connection_status = user.connectionStatus, + deleted = user.deleted, + supported_protocols = user.supportedProtocols, + active_one_on_one_conversation_id = user.activeOneOnOneConversationId + ) + } } } } @@ -299,8 +304,15 @@ class UserDAOImpl internal constructor( userQueries.deleteUser(qualifiedID) } - override suspend fun markUserAsDeleted(qualifiedID: QualifiedIDEntity) = withContext(queriesContext) { - userQueries.markUserAsDeleted(user_type = UserTypeEntity.NONE, qualified_id = qualifiedID) + override suspend fun markUserAsDeletedAndRemoveFromGroupConv(qualifiedID: QualifiedIDEntity) = withContext(queriesContext) { + userQueries.transaction { + safeMarkAsDeleted(qualifiedID) + } + } + + private fun safeMarkAsDeleted(qualifiedID: QualifiedIDEntity) { + userQueries.markUserAsDeleted(qualifiedID, UserTypeEntity.NONE) + userQueries.deleteUserFromGroupConversations(qualifiedID) } override suspend fun markUserAsDefederated(qualifiedID: QualifiedIDEntity) { diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt index 16a390b2818..b075a5f7034 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt @@ -90,7 +90,6 @@ interface ConversationDAO { suspend fun updateConversationName(conversationId: QualifiedIDEntity, conversationName: String, timestamp: String) suspend fun updateConversationType(conversationID: QualifiedIDEntity, type: ConversationEntity.Type) suspend fun updateConversationProtocol(conversationId: QualifiedIDEntity, protocol: ConversationEntity.Protocol): Boolean - suspend fun revokeOneOnOneConversationsWithDeletedUser(userId: UserIDEntity) suspend fun getConversationsByUserId(userId: UserIDEntity): List suspend fun updateConversationReceiptMode(conversationID: QualifiedIDEntity, receiptMode: ConversationEntity.ReceiptMode) suspend fun updateGuestRoomLink( diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index a8442ac6821..023cb538cd5 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -336,10 +336,6 @@ internal class ConversationDAOImpl internal constructor( } } - override suspend fun revokeOneOnOneConversationsWithDeletedUser(userId: UserIDEntity) = withContext(coroutineContext) { - memberQueries.deleteUserFromGroupConversations(userId, userId) - } - override suspend fun getConversationsByUserId(userId: UserIDEntity): List = withContext(coroutineContext) { memberQueries.selectConversationsByMember(userId, conversationMapper::toModel).executeAsList() } 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 7db01e2eead..c7c6837eade 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 @@ -20,6 +20,7 @@ package com.wire.kalium.persistence.dao import app.cash.turbine.test import com.wire.kalium.persistence.BaseDatabaseTest +import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.member.MemberEntity import com.wire.kalium.persistence.db.UserDatabaseBuilder import com.wire.kalium.persistence.utils.stubs.TestStubs @@ -464,6 +465,24 @@ class UserDAOTest : BaseDatabaseTest() { } } + @Test + fun givenDeletedUser_whenInserting_thenDoNotOverrideOldData() = runTest(dispatcher) { + // given + val commonPrefix = "common" + + val mockUser = USER_ENTITY_1.copy(name = commonPrefix + "u1", email = "test@wire.com") + db.userDAO.upsertUser(mockUser) + + val deletedUser = mockUser.copy(name = null, deleted = true, email = null) + db.userDAO.upsertUser(deletedUser) + + // when + db.userDAO.observeUserDetailsByQualifiedID(USER_ENTITY_1.id).first().also { searchResult -> + // then + assertEquals(mockUser.copy(deleted = true, team = null, userType = UserTypeEntity.NONE), searchResult?.toSimpleEntity()) + } + } + @Test fun givenAExistingUsers_whenUpdatingTheirValues_ThenResultsIsEqualToThatUserButWithFieldsModified() = runTest(dispatcher) { // given @@ -496,6 +515,74 @@ class UserDAOTest : BaseDatabaseTest() { assertNotNull(inserted2) } + @Test + fun givenExistingUsers_whenMarkUserAsDeletedAndRemoveFromGroupConv_thenRetainBasicInformation() = runTest { + val user = newUserEntity().copy(id = UserIDEntity("user-1", "domain-1")) + val groupConversation = + newConversationEntity(id = ConversationIDEntity("conversationId", "domain")).copy(type = ConversationEntity.Type.GROUP) + val oneOnOneConversation = + newConversationEntity(id = ConversationIDEntity("conversationId1on1", "domain")).copy(type = ConversationEntity.Type.ONE_ON_ONE) + db.userDAO.upsertUsers(listOf(user)) + db.conversationDAO.insertConversation(groupConversation) + db.conversationDAO.insertConversation(oneOnOneConversation) + db.memberDAO.insertMember(MemberEntity(user.id, MemberEntity.Role.Member), groupConversation.id) + db.memberDAO.insertMember(MemberEntity(user.id, MemberEntity.Role.Member), oneOnOneConversation.id) + // when + db.userDAO.markUserAsDeletedAndRemoveFromGroupConv(user.id) + + // then + db.userDAO.getAllUsersDetails().first().firstOrNull { it.id == user.id }.also { + assertNotNull(it) + assertTrue { it.deleted } + assertEquals(user.name, it.name) + assertEquals(user.handle, it.handle) + assertEquals(user.email, it.email) + assertEquals(user.phone, it.phone) + } + + db.memberDAO.observeIsUserMember(userId = user.id, conversationId = groupConversation.id).first().also { + assertFalse(it) + } + + db.memberDAO.observeIsUserMember(userId = user.id, conversationId = oneOnOneConversation.id).first().also { + assertTrue(it) + } + } + + @Test + fun givenExistingUsers_whenUpsertToDeleted_thenRetainBasicInformation() = runTest { + val user = newUserEntity().copy(id = UserIDEntity("user-1", "domain-1")) + val groupConversation = + newConversationEntity(id = ConversationIDEntity("conversationId", "domain")).copy(type = ConversationEntity.Type.GROUP) + val oneOnOneConversation = + newConversationEntity(id = ConversationIDEntity("conversationId1on1", "domain")).copy(type = ConversationEntity.Type.ONE_ON_ONE) + db.userDAO.upsertUsers(listOf(user)) + db.conversationDAO.insertConversation(groupConversation) + db.conversationDAO.insertConversation(oneOnOneConversation) + db.memberDAO.insertMember(MemberEntity(user.id, MemberEntity.Role.Member), groupConversation.id) + db.memberDAO.insertMember(MemberEntity(user.id, MemberEntity.Role.Member), oneOnOneConversation.id) + // when + db.userDAO.upsertUser(user.copy(deleted = true)) + + // then + db.userDAO.getAllUsersDetails().first().firstOrNull { it.id == user.id }.also { + assertNotNull(it) + assertTrue { it.deleted } + assertEquals(user.name, it.name) + assertEquals(user.handle, it.handle) + assertEquals(user.email, it.email) + assertEquals(user.phone, it.phone) + } + + db.memberDAO.observeIsUserMember(userId = user.id, conversationId = groupConversation.id).first().also { + assertFalse(it) + } + + db.memberDAO.observeIsUserMember(userId = user.id, conversationId = oneOnOneConversation.id).first().also { + assertTrue(it) + } + } + @Test fun givenAExistingUsers_whenUpsertingTeamMembersUserTypes_ThenUserTypeIsUpdated() = runTest(dispatcher) { // given @@ -583,7 +670,7 @@ class UserDAOTest : BaseDatabaseTest() { val user = user1 db.userDAO.upsertUser(user) val deletedUser = user1.copy(deleted = true, team = null, userType = UserTypeEntity.NONE) - db.userDAO.markUserAsDeleted(user1.id) + db.userDAO.markUserAsDeletedAndRemoveFromGroupConv(user1.id) val result = db.userDAO.observeUserDetailsByQualifiedID(user1.id).first() assertEquals(result?.toSimpleEntity(), deletedUser) @@ -698,6 +785,7 @@ class UserDAOTest : BaseDatabaseTest() { assertNotNull(result) assertEquals(true, result.defederated) } + @Test fun givenAnExistingUser_whenUpdatingTheSupportedProtocols_thenTheValueShouldBeUpdated() = runTest(dispatcher) { // given @@ -780,7 +868,7 @@ class UserDAOTest : BaseDatabaseTest() { activeOneOnOneConversationId = null, ) db.userDAO.upsertUser(user) - val updatedTeamMemberUser = user1.copy( + val updatedTeamMemberUser = user.copy( name = "newName", handle = "newHandle", email = "newEmail", @@ -793,7 +881,7 @@ class UserDAOTest : BaseDatabaseTest() { availabilityStatus = UserAvailabilityStatusEntity.BUSY, userType = UserTypeEntity.EXTERNAL, botService = BotIdEntity("newBotService", "newBotServiceDomain"), - deleted = true, + deleted = false, hasIncompleteMetadata = true, expiresAt = DateTimeUtil.currentInstant(), defederated = true,