From 22ca967f441bef469e7767764f36bb4d51eb27a6 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 13 Jan 2025 18:28:26 +0100 Subject: [PATCH 1/9] feat: database logger --- .../feature/debug/ChangeProfilingUseCase.kt | 9 +- .../kalium/logic/feature/debug/DebugScope.kt | 6 +- .../ObserveDatabaseLoggerStateUseCase.kt | 31 +++++ .../kalium/persistence/db/UserDatabase.kt | 1 - .../kalium/persistence/db/DebugExtension.kt | 107 ++++++++++++++++++ .../persistence/db/UserDatabaseBuilder.kt | 27 ++--- 6 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt create mode 100644 persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt index 83588fea472..211a7586add 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.feature.debug import com.wire.kalium.logic.di.UserStorage +import com.wire.kalium.persistence.db.DBProfile class ChangeProfilingUseCase( private val userStorage: UserStorage, @@ -26,7 +27,11 @@ class ChangeProfilingUseCase( * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted * @param enabled true to enable profiling, false to disable */ - operator fun invoke(enabled: Boolean) { - userStorage.database.changeProfiling(enabled) + suspend operator fun invoke(status: DBProfile) { + userStorage.database.debugExtension.changeProfiling(status) + } + + suspend operator fun invoke(enabled: Boolean) { + userStorage.database.debugExtension.changeProfiling(if (enabled) DBProfile.ON.Device else DBProfile.Off) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt index 34b31cdf4b7..a6da4e48459 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt @@ -93,7 +93,7 @@ class DebugScope internal constructor( private val legalHoldHandler: LegalHoldHandler, private val notificationTokenRepository: NotificationTokenRepository, private val scope: CoroutineScope, - userStorage: UserStorage, + private val userStorage: UserStorage, logger: KaliumLogger, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { @@ -227,5 +227,7 @@ class DebugScope internal constructor( notificationTokenRepository, ) - val changeProfiling: ChangeProfilingUseCase = ChangeProfilingUseCase(userStorage) + val changeProfiling: ChangeProfilingUseCase get() = ChangeProfilingUseCase(userStorage) + + val observeDatabaseLoggerState get() = ObserveDatabaseLoggerStateUseCase(userStorage) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt new file mode 100644 index 00000000000..468fe82fe60 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt @@ -0,0 +1,31 @@ +/* + * 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.feature.debug + +import com.wire.kalium.logic.di.UserStorage +import com.wire.kalium.persistence.db.DBProfile +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class ObserveDatabaseLoggerStateUseCase( + private val userStorage: UserStorage, +) { + suspend operator fun invoke(): Flow = userStorage.database.debugExtension.getProfilingState().map { + it is DBProfile.ON + } +} diff --git a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index 4491d1bf47c..84f63d0a743 100644 --- a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -57,7 +57,6 @@ actual fun userDatabaseBuilder( dispatcher = dispatcher, platformDatabaseData = platformDatabaseData, isEncrypted = isEncryptionEnabled, - cipherProfile = "logcat", ) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt new file mode 100644 index 00000000000..f9dfd619555 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt @@ -0,0 +1,107 @@ +/* + * 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.persistence.db + +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver +import com.wire.kalium.persistence.dao.MetadataDAO +import com.wire.kalium.persistence.kaliumLogger +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class DebugExtension( + private val sqlDriver: SqlDriver, + private val isEncrypted: Boolean, + private val metaDataDao: MetadataDAO, +) { + + suspend fun getProfilingState(): Flow = + metaDataDao.valueByKeyFlow(KEY_CIPHER_PROFILE) + .map { + it?.let { DBProfile.fromString(it) } + } + + /** + * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted + * @param enabled true to enable profiling, false to disable + */ + suspend fun changeProfiling(state: DBProfile): Long? = + if (isEncrypted) { + sqlDriver.executeQuery( + identifier = null, + sql = """PRAGMA cipher_profile= '${state.logTarget}';""", + mapper = { cursor -> + cursor.next() + cursor.getLong(0).let { QueryResult.Value(it) } + }, + parameters = 0, + ).value.also { + updateMetadata(state) + } + + } else { + error("Cannot change profiling on unencrypted database") + } + + private suspend fun updateMetadata(state: DBProfile) { + metaDataDao.insertValue( + value = state.logTarget, + key = KEY_CIPHER_PROFILE + ) + } + + private companion object { + const val KEY_CIPHER_PROFILE = "cipher_profile" + } +} + +sealed interface DBProfile { + val logTarget: String + + data object Off : DBProfile { + override val logTarget: String = "off" + + override fun toString(): String { + return "off" + } + } + + sealed interface ON : DBProfile { + data object Device : ON { + override val logTarget: String = "logcat" + + override fun toString(): String { + return "logcat" + } + } + + data class CustomFile(override val logTarget: String) : ON { + override fun toString(): String { + return logTarget + } + } + } + + companion object { + fun fromString(value: String): DBProfile = when (value) { + "off" -> Off + "logcat" -> ON.Device + else -> ON.CustomFile(value) + } + } +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index 76f9ab33e17..7bbebca1f26 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -120,7 +120,6 @@ class UserDatabaseBuilder internal constructor( private val platformDatabaseData: PlatformDatabaseData, private val isEncrypted: Boolean, private val queriesContext: CoroutineContext = KaliumDispatcherImpl.io, - private val cipherProfile: String? = null, ) { internal val database: UserDatabase = UserDatabase( @@ -314,30 +313,18 @@ class UserDatabaseBuilder internal constructor( queriesContext ) + val debugExtension: DebugExtension + get() = DebugExtension( + sqlDriver = sqlDriver, + metaDataDao = metadataDAO, + isEncrypted = isEncrypted + ) + /** * @return the absolute path of the DB file or null if the DB file does not exist */ fun dbFileLocation(): String? = getDatabaseAbsoluteFileLocation(platformDatabaseData, userId) - /** - * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted - * @param enabled true to enable profiling, false to disable - */ - fun changeProfiling(enabled: Boolean) { - if (isEncrypted && cipherProfile != null) { - val cipherProfileValue = if (enabled) cipherProfile else "off" - sqlDriver.executeQuery( - identifier = null, - sql = "PRAGMA cipher_profile='$cipherProfileValue'", - mapper = { - it.next() - it.getLong(0).let { QueryResult.Value(it) } - }, - parameters = 0, - ) - } - } - /** * drops DB connection and delete the DB file */ From 27f090091e5222744be09262744e4e5b48f08ef3 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Mon, 13 Jan 2025 18:32:49 +0100 Subject: [PATCH 2/9] detekt --- .../logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt | 3 +++ .../kotlin/com/wire/kalium/persistence/db/DebugExtension.kt | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt index 468fe82fe60..537e159ed96 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt @@ -22,6 +22,9 @@ import com.wire.kalium.persistence.db.DBProfile import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +/** + * Use case to observe the state of the database logger. + */ class ObserveDatabaseLoggerStateUseCase( private val userStorage: UserStorage, ) { diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt index f9dfd619555..81771f65b82 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt @@ -20,7 +20,6 @@ package com.wire.kalium.persistence.db import app.cash.sqldelight.db.QueryResult import app.cash.sqldelight.db.SqlDriver import com.wire.kalium.persistence.dao.MetadataDAO -import com.wire.kalium.persistence.kaliumLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map From ef73962fc324d9355f8958a9e46c67222bc34d11 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Tue, 14 Jan 2025 11:23:57 +0100 Subject: [PATCH 3/9] detekt --- .../logic/feature/debug/ChangeProfilingUseCase.kt | 10 ++-------- .../debug/ObserveDatabaseLoggerStateUseCase.kt | 6 +----- .../com/wire/kalium/persistence/db/DebugExtension.kt | 11 +++++++---- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt index 211a7586add..fc5ee605a03 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ChangeProfilingUseCase.kt @@ -18,20 +18,14 @@ package com.wire.kalium.logic.feature.debug import com.wire.kalium.logic.di.UserStorage -import com.wire.kalium.persistence.db.DBProfile class ChangeProfilingUseCase( private val userStorage: UserStorage, ) { /** - * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted - * @param enabled true to enable profiling, false to disable + * Change profiling state. */ - suspend operator fun invoke(status: DBProfile) { - userStorage.database.debugExtension.changeProfiling(status) - } - suspend operator fun invoke(enabled: Boolean) { - userStorage.database.debugExtension.changeProfiling(if (enabled) DBProfile.ON.Device else DBProfile.Off) + userStorage.database.debugExtension.changeProfiling(enabled) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt index 537e159ed96..c4fd40af94d 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/ObserveDatabaseLoggerStateUseCase.kt @@ -18,9 +18,7 @@ package com.wire.kalium.logic.feature.debug import com.wire.kalium.logic.di.UserStorage -import com.wire.kalium.persistence.db.DBProfile import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map /** * Use case to observe the state of the database logger. @@ -28,7 +26,5 @@ import kotlinx.coroutines.flow.map class ObserveDatabaseLoggerStateUseCase( private val userStorage: UserStorage, ) { - suspend operator fun invoke(): Flow = userStorage.database.debugExtension.getProfilingState().map { - it is DBProfile.ON - } + suspend operator fun invoke(): Flow = userStorage.database.debugExtension.observeIsProfilingEnabled() } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt index 81771f65b82..2da20e54bb2 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt @@ -29,18 +29,21 @@ class DebugExtension( private val metaDataDao: MetadataDAO, ) { - suspend fun getProfilingState(): Flow = + suspend fun observeIsProfilingEnabled(): Flow = metaDataDao.valueByKeyFlow(KEY_CIPHER_PROFILE) - .map { - it?.let { DBProfile.fromString(it) } + .map { state -> + state?.let { DBProfile.fromString(it) }.let { + it is DBProfile.ON + } } /** * Changes the profiling of the database (cipher_profile) if the profile is specified and the database is encrypted * @param enabled true to enable profiling, false to disable */ - suspend fun changeProfiling(state: DBProfile): Long? = + suspend fun changeProfiling(enabled: Boolean): Long? = if (isEncrypted) { + val state = if (enabled) DBProfile.ON.Device else DBProfile.Off sqlDriver.executeQuery( identifier = null, sql = """PRAGMA cipher_profile= '${state.logTarget}';""", From 2d89df8e8879d714d95a49fb5838790830f60df2 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Tue, 14 Jan 2025 14:42:30 +0100 Subject: [PATCH 4/9] set logger name per platform --- .../persistence/db/DebugExtension.android.kt | 20 +++++++++++++++++ .../persistence/db/DebugExtension.apple.kt | 20 +++++++++++++++++ .../kalium/persistence/db/DebugExtension.kt | 6 +++-- .../persistence/db/DebugExtension.js.kt | 22 +++++++++++++++++++ .../persistence/db/DebugExtension.jvm.kt | 22 +++++++++++++++++++ 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.android.kt create mode 100644 persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.apple.kt create mode 100644 persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.js.kt create mode 100644 persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.jvm.kt diff --git a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.android.kt b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.android.kt new file mode 100644 index 00000000000..86825694070 --- /dev/null +++ b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.android.kt @@ -0,0 +1,20 @@ +/* + * 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.persistence.db + +internal actual fun platformDatabaseLogger(): String = "logcat" diff --git a/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.apple.kt b/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.apple.kt new file mode 100644 index 00000000000..0d5faf02c18 --- /dev/null +++ b/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.apple.kt @@ -0,0 +1,20 @@ +/* + * 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.persistence.db + +internal actual fun platformDatabaseLogger(): String = "os_log" diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt index 2da20e54bb2..a2a232ecd3c 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.kt @@ -88,7 +88,7 @@ sealed interface DBProfile { override val logTarget: String = "logcat" override fun toString(): String { - return "logcat" + return platformDatabaseLogger() } } @@ -102,8 +102,10 @@ sealed interface DBProfile { companion object { fun fromString(value: String): DBProfile = when (value) { "off" -> Off - "logcat" -> ON.Device + platformDatabaseLogger() -> ON.Device else -> ON.CustomFile(value) } } } + +internal expect fun platformDatabaseLogger(): String diff --git a/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.js.kt b/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.js.kt new file mode 100644 index 00000000000..232bf463761 --- /dev/null +++ b/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.js.kt @@ -0,0 +1,22 @@ +/* + * 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.persistence.db + +internal actual fun platformDatabaseLogger(): String { + TODO("Not yet implemented") +} diff --git a/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.jvm.kt b/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.jvm.kt new file mode 100644 index 00000000000..232bf463761 --- /dev/null +++ b/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/DebugExtension.jvm.kt @@ -0,0 +1,22 @@ +/* + * 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.persistence.db + +internal actual fun platformDatabaseLogger(): String { + TODO("Not yet implemented") +} From 86f7655459c46fea1a6266fb42dc051519e2ca75 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Tue, 14 Jan 2025 16:08:47 +0100 Subject: [PATCH 5/9] Trigger CI From b016dd9f6d3d053be275fe57e3b50ecb68b32d14 Mon Sep 17 00:00:00 2001 From: Mohamad Jaara Date: Tue, 14 Jan 2025 17:53:08 +0100 Subject: [PATCH 6/9] fix test --- .../persistence/dao/client/ClientDAOTest.kt | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt index 4fa472f3ee1..1f871df56f9 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOTest.kt @@ -484,44 +484,6 @@ class ClientDAOTest : BaseDatabaseTest() { assertNull(clientDAO.isMLSCapable(userId, clientId = client.id)) } - @Test - fun givenPersistedClient_whenUpsertingTheSameExactClient_thenItShouldIgnoreAndNotNotifyOtherQueries() = runTest { - // Given - userDAO.upsertUser(user) - clientDAO.insertClient(insertedClient) - - clientDAO.observeClient(user.id, insertedClient.id).test { - val initialValue = awaitItem() - assertEquals(insertedClient.toClient(), initialValue) - - // When - clientDAO.insertClient(insertedClient) // the same exact client is being saved again - - // Then - expectNoEvents() // other query should not be notified - } - } - - @Test - fun givenPersistedClient_whenUpsertingUpdatedClient_thenItShouldBeSavedAndOtherQueriesShouldBeUpdated() = runTest { - // Given - userDAO.upsertUser(user) - clientDAO.insertClient(insertedClient) - val updatedInsertedClient = insertedClient.copy(label = "new_label") - - clientDAO.observeClient(user.id, insertedClient.id).test { - val initialValue = awaitItem() - assertEquals(insertedClient.toClient(), initialValue) - - // When - clientDAO.insertClient(updatedInsertedClient) // updated client is being saved that should replace the old one - - // Then - val updatedValue = awaitItem() // other query should be notified - assertEquals(updatedInsertedClient.toClient(), updatedValue) - } - } - private companion object { val userId = QualifiedIDEntity("test", "domain") val user = newUserEntity(userId) From fa3b5d28a85501b9a12962c45a49cd85786d1b28 Mon Sep 17 00:00:00 2001 From: Boris Safonov Date: Mon, 20 Jan 2025 12:06:52 +0200 Subject: [PATCH 7/9] feat: Clear conversation content on all devices --- .../conversation/ConversationRepository.kt | 45 +++++- .../kalium/logic/feature/UserSessionScope.kt | 6 +- .../ClearConversationContentUseCase.kt | 62 ++++---- .../feature/conversation/ConversationScope.kt | 19 +-- .../DeleteConversationLocallyUseCase.kt | 14 +- .../conversation/MemberLeaveEventHandler.kt | 29 +++- .../ClearConversationContentHandler.kt | 45 ++++-- .../ConversationRepositoryTest.kt | 7 +- .../ClearConversationContentUseCaseTest.kt | 101 ++++++------ .../DeleteConversationLocallyUseCaseTest.kt | 65 +++----- .../ClearConversationContentHandlerTest.kt | 144 ++++++++++++------ .../MemberLeaveEventHandlerTest.kt | 49 +++++- .../ConversationRepositoryArrangement.kt | 25 +++ 13 files changed, 396 insertions(+), 215 deletions(-) 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 6277230a283..e5154518256 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 @@ -66,6 +66,8 @@ import com.wire.kalium.network.api.authenticated.conversation.model.Conversation import com.wire.kalium.network.api.authenticated.conversation.model.ConversationReceiptModeDTO import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi +import com.wire.kalium.persistence.dao.MetadataDAO +import com.wire.kalium.persistence.dao.MetadataDAOImpl import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.client.ClientDAO import com.wire.kalium.persistence.dao.conversation.ConversationDAO @@ -85,6 +87,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant +import kotlinx.serialization.builtins.SetSerializer @Suppress("TooManyFunctions") interface ConversationRepository { @@ -224,7 +227,6 @@ interface ConversationRepository { ): Either suspend fun deleteConversation(conversationId: ConversationId): Either - suspend fun deleteConversationLocally(conversationId: ConversationId): Either /** * Deletes all conversation messages @@ -315,6 +317,9 @@ interface ConversationRepository { suspend fun getGroupStatusMembersNamesAndHandles(groupID: GroupID): Either suspend fun selectMembersNameAndHandle(conversationId: ConversationId): Either> + suspend fun addConversationToDeleteQueue(conversationId: ConversationId) + suspend fun removeConversationFromDeleteQueue(conversationId: ConversationId) + suspend fun getConversationsDeleteQueue(): List } @Suppress("LongParameterList", "TooManyFunctions", "LargeClass") @@ -330,6 +335,7 @@ internal class ConversationDataSource internal constructor( private val clientDAO: ClientDAO, private val clientApi: ClientApi, private val conversationMetaDataDAO: ConversationMetaDataDAO, + private val metadataDAO: MetadataDAO, private val idMapper: IdMapper = MapperProvider.idMapper(), private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId), private val memberMapper: MemberMapper = MapperProvider.memberMapper(), @@ -884,12 +890,6 @@ internal class ConversationDataSource internal constructor( } } - override suspend fun deleteConversationLocally(conversationId: ConversationId): Either { - return wrapStorageRequest { - conversationDAO.deleteConversationByQualifiedID(conversationId.toDao()) - } - } - override suspend fun clearContent(conversationId: ConversationId): Either = wrapStorageRequest { conversationDAO.clearContent(conversationId.toDao()) @@ -1146,7 +1146,38 @@ internal class ConversationDataSource internal constructor( .mapKeys { it.key.toModel() } } + override suspend fun addConversationToDeleteQueue(conversationId: ConversationId) { + val queue = metadataDAO.getSerializable(CONVERSATIONS_TO_DELETE_KEY, SetSerializer(QualifiedIDEntity.serializer())) + ?.toMutableSet() + ?.plus(conversationId.toDao()) + ?: setOf(conversationId.toDao()) + + metadataDAO.putSerializable( + CONVERSATIONS_TO_DELETE_KEY, + queue, + SetSerializer(QualifiedIDEntity.serializer()) + ) + } + + override suspend fun removeConversationFromDeleteQueue(conversationId: ConversationId) { + val queue = metadataDAO.getSerializable(CONVERSATIONS_TO_DELETE_KEY, SetSerializer(QualifiedIDEntity.serializer())) + ?.toMutableSet() + ?.minus(conversationId.toDao()) + ?: return + + metadataDAO.putSerializable( + CONVERSATIONS_TO_DELETE_KEY, + queue, + SetSerializer(QualifiedIDEntity.serializer()) + ) + } + + override suspend fun getConversationsDeleteQueue(): List = + metadataDAO.getSerializable(CONVERSATIONS_TO_DELETE_KEY, SetSerializer(QualifiedIDEntity.serializer())) + ?.map { it.toModel() } ?: listOf() + companion object { const val DEFAULT_MEMBER_ROLE = "wire_member" + private const val CONVERSATIONS_TO_DELETE_KEY = "conversations_to_delete" } } 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 14bdb75e474..2e7e7b70f1c 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 @@ -732,6 +732,7 @@ class UserSessionScope internal constructor( userStorage.database.clientDAO, authenticatedNetworkContainer.clientApi, userStorage.database.conversationMetaDataDAO, + userStorage.database.metadataDAO, ) private val conversationFolderRepository: ConversationFolderRepository @@ -1381,6 +1382,7 @@ class UserSessionScope internal constructor( conversationRepository, userId, isMessageSentInSelfConversation, + conversations.clearConversationAssetsLocally ), DeleteForMeHandlerImpl(messageRepository, isMessageSentInSelfConversation), DeleteMessageHandlerImpl(messageRepository, assetRepository, NotificationEventsManagerImpl, userId), @@ -1441,10 +1443,12 @@ class UserSessionScope internal constructor( get() = MemberLeaveEventHandlerImpl( memberDAO = userStorage.database.memberDAO, userRepository = userRepository, + conversationRepository = conversationRepository, persistMessage = persistMessage, updateConversationClientsForCurrentCall = updateConversationClientsForCurrentCall, legalHoldHandler = legalHoldHandler, - selfTeamIdProvider = selfTeamId + selfTeamIdProvider = selfTeamId, + selfUserId = userId ) private val memberChangeHandler: MemberChangeEventHandler get() = MemberChangeEventHandlerImpl( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt index d884b86693f..cb29b3dd24b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCase.kt @@ -19,13 +19,14 @@ package com.wire.kalium.logic.feature.conversation import com.benasher44.uuid.uuid4 +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.cache.SelfConversationIdProvider import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold @@ -41,11 +42,11 @@ interface ClearConversationContentUseCase { * @param conversationId The conversation id to clear all messages. * @return [Result] of the operation, indicating success or failure. */ - suspend operator fun invoke(conversationId: ConversationId): Result + suspend operator fun invoke(conversationId: ConversationId, needToRemoveConversation: Boolean = false): Result sealed class Result { data object Success : Result() - data object Failure : Result() + data class Failure(val failure: CoreFailure) : Result() } } @@ -54,33 +55,38 @@ internal class ClearConversationContentUseCaseImpl( private val messageSender: MessageSender, private val selfUserId: UserId, private val currentClientIdProvider: CurrentClientIdProvider, - private val selfConversationIdProvider: SelfConversationIdProvider + private val selfConversationIdProvider: SelfConversationIdProvider, + private val clearLocalConversationAssets: ClearConversationAssetsLocallyUseCase ) : ClearConversationContentUseCase { - override suspend fun invoke(conversationId: ConversationId): ClearConversationContentUseCase.Result = - conversationRepository.clearContent(conversationId).flatMap { - currentClientIdProvider().flatMap { currentClientId -> - selfConversationIdProvider().flatMap { selfConversationIds -> - selfConversationIds.foldToEitherWhileRight(Unit) { selfConversationId, _ -> - val regularMessage = Message.Signaling( - id = uuid4().toString(), - content = MessageContent.Cleared( - conversationId = conversationId, - time = DateTimeUtil.currentInstant(), - needToRemoveLocally = false // TODO Handle in upcoming tasks - ), - // sending the message to clear this conversation - conversationId = selfConversationId, - date = Clock.System.now(), - senderUserId = selfUserId, - senderClientId = currentClientId, - status = Message.Status.Pending, - isSelfMessage = true, - expirationData = null - ) - messageSender.sendMessage(regularMessage) - } + override suspend fun invoke( + conversationId: ConversationId, + needToRemoveConversation: Boolean + ): ClearConversationContentUseCase.Result = + currentClientIdProvider().flatMap { currentClientId -> + selfConversationIdProvider().flatMap { selfConversationIds -> + selfConversationIds.foldToEitherWhileRight(Unit) { selfConversationId, _ -> + val regularMessage = Message.Signaling( + id = uuid4().toString(), + content = MessageContent.Cleared( + conversationId = conversationId, + time = DateTimeUtil.currentInstant(), + needToRemoveLocally = needToRemoveConversation + ), + // sending the message to clear this conversation + conversationId = selfConversationId, + date = Clock.System.now(), + senderUserId = selfUserId, + senderClientId = currentClientId, + status = Message.Status.Pending, + isSelfMessage = true, + expirationData = null + ) + messageSender.sendMessage(regularMessage) } } - }.fold({ ClearConversationContentUseCase.Result.Failure }, { ClearConversationContentUseCase.Result.Success }) + } + .flatMap { conversationRepository.clearContent(conversationId) } + .flatMap { clearLocalConversationAssets(conversationId) } + .fold({ ClearConversationContentUseCase.Result.Failure(it) }, { ClearConversationContentUseCase.Result.Success }) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index 842a6765fc8..00da8d24549 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -268,25 +268,26 @@ class ConversationScope internal constructor( val updateMLSGroupsKeyingMaterials: UpdateKeyingMaterialsUseCase get() = UpdateKeyingMaterialsUseCaseImpl(mlsConversationRepository, updateKeyingMaterialThresholdProvider) + val clearConversationAssetsLocally: ClearConversationAssetsLocallyUseCase + get() = ClearConversationAssetsLocallyUseCaseImpl( + messageRepository, + assetRepository + ) + val clearConversationContent: ClearConversationContentUseCase get() = ClearConversationContentUseCaseImpl( conversationRepository, messageSender, selfUserId, currentClientIdProvider, - selfConversationIdProvider - ) - - val clearConversationAssetsLocally: ClearConversationAssetsLocallyUseCase - get() = ClearConversationAssetsLocallyUseCaseImpl( - messageRepository, - assetRepository + selfConversationIdProvider, + clearConversationAssetsLocally ) val deleteConversationLocallyUseCase: DeleteConversationLocallyUseCase get() = DeleteConversationLocallyUseCaseImpl( - conversationRepository, - clearConversationAssetsLocally + clearConversationContent, + conversationRepository ) val joinConversationViaCode: JoinConversationViaCodeUseCase diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.kt index aaf07b999b3..fff34b2bdb0 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCase.kt @@ -21,7 +21,7 @@ import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.functional.Either -import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.left interface DeleteConversationLocallyUseCase { /** @@ -29,6 +29,7 @@ interface DeleteConversationLocallyUseCase { * - Clear all local assets * - Clear content * - Remove conversation + * - Send Signal message to other clients to do the same * * @param conversationId - id of conversation to delete */ @@ -36,13 +37,16 @@ interface DeleteConversationLocallyUseCase { } internal class DeleteConversationLocallyUseCaseImpl( + private val clearConversationContent: ClearConversationContentUseCase, private val conversationRepository: ConversationRepository, - private val clearLocalConversationAssets: ClearConversationAssetsLocallyUseCase ) : DeleteConversationLocallyUseCase { override suspend fun invoke(conversationId: ConversationId): Either { - return clearLocalConversationAssets(conversationId) - .flatMap { conversationRepository.clearContent(conversationId) } - .flatMap { conversationRepository.deleteConversationLocally(conversationId) } + val clearResult = clearConversationContent(conversationId, true) + return if (clearResult is ClearConversationContentUseCase.Result.Failure) { + clearResult.failure.left() + } else { + conversationRepository.deleteConversation(conversationId) + } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt index cebc0778af7..a6686b31c6a 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.sync.receiver.conversation import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.event.MemberLeaveReason import com.wire.kalium.logic.data.id.ConversationId @@ -49,23 +50,23 @@ interface MemberLeaveEventHandler { internal class MemberLeaveEventHandlerImpl( private val memberDAO: MemberDAO, private val userRepository: UserRepository, + private val conversationRepository: ConversationRepository, private val persistMessage: PersistMessageUseCase, private val updateConversationClientsForCurrentCall: Lazy, private val legalHoldHandler: LegalHoldHandler, - private val selfTeamIdProvider: SelfTeamIdProvider + private val selfTeamIdProvider: SelfTeamIdProvider, + private val selfUserId: UserId, ) : MemberLeaveEventHandler { override suspend fun handle(event: Event.Conversation.MemberLeave): Either { val eventLogger = kaliumLogger.createEventProcessingLogger(event) - return let { - if (event.reason == MemberLeaveReason.UserDeleted) { - userRepository.markAsDeleted(event.removedList) - } - deleteMembers(event.removedList, event.conversationId) + if (event.reason == MemberLeaveReason.UserDeleted) { + userRepository.markAsDeleted(event.removedList) } + return deleteMembers(event.removedList, event.conversationId) + .onSuccess { updateConversationClientsForCurrentCall.value(event.conversationId) } + .onSuccess { deleteConversationIfNeeded(event) } .onSuccess { - updateConversationClientsForCurrentCall.value(event.conversationId) - }.onSuccess { // fetch required unknown users that haven't been persisted during slow sync, e.g. from another team // and keep them to properly show this member-leave message userRepository.fetchUsersIfUnknownByIds(event.removedList.toSet()) @@ -131,4 +132,16 @@ internal class MemberLeaveEventHandlerImpl( conversationID.toDao() ) } + + private suspend fun deleteConversationIfNeeded(event: Event.Conversation.MemberLeave) { + val isSelfUserLeftConversation = event.removedList == listOf(selfUserId) && event.reason == MemberLeaveReason.Left + if (!isSelfUserLeftConversation) return + + if (!conversationRepository.getConversationsDeleteQueue().contains(event.conversationId)) return + + // User wanted to delete conversation fully, but MessageContent.Cleared event came before and we couldn't delete it then. + // Now, when user left the conversation, we can delete it. + conversationRepository.deleteConversation(event.conversationId) + conversationRepository.removeConversationFromDeleteQueue(event.conversationId) + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt index 044e6efe9df..0ddfb903bb7 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/ClearConversationContentHandler.kt @@ -19,10 +19,13 @@ package com.wire.kalium.logic.sync.receiver.handler import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.IsMessageSentInSelfConversationUseCase +import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.data.message.Message -import com.wire.kalium.logic.data.message.IsMessageSentInSelfConversationUseCase +import com.wire.kalium.logic.feature.conversation.ClearConversationAssetsLocallyUseCase +import com.wire.kalium.logic.functional.fold internal interface ClearConversationContentHandler { suspend fun handle( @@ -34,25 +37,41 @@ internal interface ClearConversationContentHandler { internal class ClearConversationContentHandlerImpl( private val conversationRepository: ConversationRepository, private val selfUserId: UserId, - private val isMessageSentInSelfConversation: IsMessageSentInSelfConversationUseCase + private val isMessageSentInSelfConversation: IsMessageSentInSelfConversationUseCase, + private val clearLocalConversationAssets: ClearConversationAssetsLocallyUseCase ) : ClearConversationContentHandler { override suspend fun handle( message: Message.Signaling, messageContent: MessageContent.Cleared ) { - val isMessageComingFromAnotherUser = message.senderUserId != selfUserId - val isMessageDestinedForSelfConversation: Boolean = isMessageSentInSelfConversation(message) + val isSelfSender = message.senderUserId == selfUserId + val isMessageInSelfConversation: Boolean = isMessageSentInSelfConversation(message) - if (isMessageComingFromAnotherUser) { - when { - !messageContent.needToRemoveLocally && !isMessageDestinedForSelfConversation -> return - messageContent.needToRemoveLocally && !isMessageDestinedForSelfConversation -> conversationRepository.deleteConversation( - messageContent.conversationId - ) + if (isSelfSender != isMessageInSelfConversation) return - else -> conversationRepository.clearContent(messageContent.conversationId) - } + clearConversation(messageContent.conversationId) + + if (messageContent.needToRemoveLocally && isSelfSender) { + tryToRemoveConversation(messageContent.conversationId) } } + + private suspend fun tryToRemoveConversation(conversationId: ConversationId) { + conversationRepository.getConversationMembers(conversationId).fold({ false }, { it.contains(selfUserId) }) + .let { isMember -> + if (isMember) { + // Sometimes MessageContent.Cleared event may come before User Left conversation event. + // In that case we couldn't delete it and should wait for user leave and delete after that. + conversationRepository.addConversationToDeleteQueue(conversationId) + } else { + conversationRepository.deleteConversation(conversationId) + } + } + } + + private suspend fun clearConversation(conversationId: ConversationId) { + conversationRepository.clearContent(conversationId) + clearLocalConversationAssets(conversationId) + } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt index a780f65d707..c2f990932b0 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt @@ -75,6 +75,7 @@ import com.wire.kalium.network.api.model.ConversationAccessRoleDTO import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.persistence.dao.ConversationIDEntity +import com.wire.kalium.persistence.dao.MetadataDAO import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.dao.client.ClientDAO @@ -1452,6 +1453,9 @@ class ConversationRepositoryTest { @Mock val conversationMetaDataDAO: ConversationMetaDataDAO = mock(ConversationMetaDataDAO::class) + @Mock + val metadataDAO: MetadataDAO = mock(MetadataDAO::class) + @Mock val renamedConversationEventHandler = mock(RenamedConversationEventHandler::class) @@ -1468,7 +1472,8 @@ class ConversationRepositoryTest { messageDraftDAO, clientDao, clientApi, - conversationMetaDataDAO + conversationMetaDataDAO, + metadataDAO ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCaseTest.kt index 86cd8ce2570..20fa98f29f6 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ClearConversationContentUseCaseTest.kt @@ -44,6 +44,7 @@ class ClearConversationContentUseCaseTest { // given val (arrangement, useCase) = Arrangement() .withClearConversationContent(false) + .withClearConversationAssetsLocally(true) .withMessageSending(true) .withCurrentClientId((true)) .withSelfConversationIds(listOf(selfConversationId)) @@ -56,17 +57,10 @@ class ClearConversationContentUseCaseTest { assertIs(result) with(arrangement) { - coVerify { - conversationRepository.clearContent(any()) - }.wasInvoked(exactly = once) - - coVerify { - currentClientIdProvider.invoke() - }.wasNotInvoked() - - coVerify { - messageSender.sendMessage(any(), any()) - }.wasNotInvoked() + coVerify { conversationRepository.clearContent(any()) }.wasInvoked(exactly = once) + coVerify { currentClientIdProvider.invoke() }.wasInvoked(exactly = once) + coVerify { messageSender.sendMessage(any(), any()) }.wasInvoked(exactly = once) + coVerify { clearConversationAssetsLocally(any()) }.wasNotInvoked() } } @@ -77,6 +71,7 @@ class ClearConversationContentUseCaseTest { .withClearConversationContent(true) .withCurrentClientId(false) .withMessageSending(true) + .withClearConversationAssetsLocally(true) .withSelfConversationIds(listOf(selfConversationId)) .arrange() @@ -87,17 +82,9 @@ class ClearConversationContentUseCaseTest { assertIs(result) with(arrangement) { - coVerify { - conversationRepository.clearContent(any()) - }.wasInvoked(exactly = once) - - coVerify { - currentClientIdProvider.invoke() - }.wasInvoked(exactly = once) - - coVerify { - messageSender.sendMessage(any(), any()) - }.wasNotInvoked() + coVerify { conversationRepository.clearContent(any()) }.wasNotInvoked() + coVerify { currentClientIdProvider.invoke() }.wasInvoked(exactly = once) + coVerify { messageSender.sendMessage(any(), any()) }.wasNotInvoked() } } @@ -108,6 +95,7 @@ class ClearConversationContentUseCaseTest { .withClearConversationContent(true) .withCurrentClientId(true) .withMessageSending(false) + .withClearConversationAssetsLocally(true) .withSelfConversationIds(listOf(selfConversationId)) .arrange() @@ -118,17 +106,34 @@ class ClearConversationContentUseCaseTest { assertIs(result) with(arrangement) { - coVerify { - conversationRepository.clearContent(any()) - }.wasInvoked(exactly = once) + coVerify { conversationRepository.clearContent(any()) }.wasNotInvoked() + coVerify { currentClientIdProvider.invoke() }.wasInvoked(exactly = once) + coVerify { messageSender.sendMessage(any(), any()) }.wasInvoked(exactly = once) + } + } + + @Test + fun givenClearAssetsFails_whenInvoking_thenCorrectlyPropagateFailure() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withClearConversationContent(true) + .withCurrentClientId(true) + .withMessageSending(true) + .withClearConversationAssetsLocally(false) + .withSelfConversationIds(listOf(selfConversationId)) + .arrange() - coVerify { - currentClientIdProvider.invoke() - }.wasInvoked(exactly = once) + // when + val result = useCase(ConversationId("someValue", "someDomain")) - coVerify { - messageSender.sendMessage(any(), any()) - }.wasInvoked(exactly = once) + // then + assertIs(result) + + with(arrangement) { + coVerify { conversationRepository.clearContent(any()) }.wasInvoked(exactly = once) + coVerify { currentClientIdProvider.invoke() }.wasInvoked(exactly = once) + coVerify { messageSender.sendMessage(any(), any()) }.wasInvoked(exactly = once) + coVerify { clearConversationAssetsLocally(any()) }.wasInvoked(exactly = once) } } @@ -139,6 +144,7 @@ class ClearConversationContentUseCaseTest { .withClearConversationContent(true) .withCurrentClientId(true) .withMessageSending(true) + .withClearConversationAssetsLocally(true) .withSelfConversationIds(listOf(selfConversationId)) .arrange() @@ -149,17 +155,9 @@ class ClearConversationContentUseCaseTest { assertIs(result) with(arrangement) { - coVerify { - conversationRepository.clearContent(any()) - }.wasInvoked(exactly = once) - - coVerify { - currentClientIdProvider.invoke() - }.wasInvoked(exactly = once) - - coVerify { - messageSender.sendMessage(any(), any()) - }.wasInvoked(exactly = once) + coVerify { conversationRepository.clearContent(any()) }.wasInvoked(exactly = once) + coVerify { currentClientIdProvider.invoke() }.wasInvoked(exactly = once) + coVerify { messageSender.sendMessage(any(), any()) }.wasInvoked(exactly = once) } } @@ -181,27 +179,33 @@ class ClearConversationContentUseCaseTest { @Mock val messageSender = mock(MessageSender::class) + @Mock + val clearConversationAssetsLocally = mock(ClearConversationAssetsLocallyUseCase::class) + suspend fun withClearConversationContent(isSuccessFull: Boolean) = apply { coEvery { conversationRepository.clearContent(any()) }.returns(if (isSuccessFull) Either.Right(Unit) else Either.Left(CoreFailure.Unknown(Throwable("an error")))) } - suspend fun withCurrentClientId(isSuccessFull: Boolean): Arrangement { + suspend fun withClearConversationAssetsLocally(isSuccessFull: Boolean) = apply { + coEvery { + clearConversationAssetsLocally(any()) + }.returns(if (isSuccessFull) Either.Right(Unit) else Either.Left(CoreFailure.Unknown(Throwable("an error")))) + } + + suspend fun withCurrentClientId(isSuccessFull: Boolean) = apply { coEvery { currentClientIdProvider() } .returns( if (isSuccessFull) Either.Right(TestClient.CLIENT_ID) else Either.Left(CoreFailure.Unknown(Throwable("an error"))) ) - return this } - suspend fun withMessageSending(isSuccessFull: Boolean): Arrangement { + suspend fun withMessageSending(isSuccessFull: Boolean) = apply { coEvery { messageSender.sendMessage(any(), any()) }.returns(if (isSuccessFull) Either.Right(Unit) else Either.Left(CoreFailure.Unknown(Throwable("an error")))) - - return this } suspend fun withSelfConversationIds(conversationIds: List) = apply { @@ -215,7 +219,8 @@ class ClearConversationContentUseCaseTest { messageSender, TestUser.SELF.id, currentClientIdProvider, - selfConversationIdProvider + selfConversationIdProvider, + clearConversationAssetsLocally ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.kt index a6abd5c4edf..9c7cca7dd08 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/DeleteConversationLocallyUseCaseTest.kt @@ -25,6 +25,7 @@ import io.mockative.Mock import io.mockative.any import io.mockative.coEvery import io.mockative.coVerify +import io.mockative.eq import io.mockative.mock import kotlinx.coroutines.test.runTest import kotlin.test.Test @@ -41,9 +42,8 @@ class DeleteConversationLocallyUseCaseTest { @Test fun givenDeleteLocalConversationInvoked_whenAllStepsAreSuccessful_thenSuccessResultIsPropagated() = runTest { // given - val (_, useCase) = Arrangement() - .withClearContent(SUCCESS) - .withClearLocalAsset(SUCCESS) + val (arrangement, useCase) = Arrangement() + .withClearLocalAsset(true) .withDeleteLocalConversation(SUCCESS) .arrange() @@ -52,15 +52,16 @@ class DeleteConversationLocallyUseCaseTest { // then assertIs>(result) + coVerify { arrangement.clearConversationContent(any(), eq(true)) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasInvoked(exactly = 1) } @Test - fun givenDeleteLocalConversationInvoked_whenAssetClearIsUnsuccessful_thenErrorResultIsPropagated() = runTest { + fun givenDeleteLocalConversationInvoked_whenDeleteConversationIsUnsuccessful_thenErrorResultIsPropagated() = runTest { // given val (arrangement, useCase) = Arrangement() - .withClearContent(SUCCESS) - .withClearLocalAsset(ERROR) - .withDeleteLocalConversation(SUCCESS) + .withClearLocalAsset(true) + .withDeleteLocalConversation(ERROR) .arrange() // when @@ -68,16 +69,15 @@ class DeleteConversationLocallyUseCaseTest { // then assertIs>(result) - coVerify { arrangement.conversationRepository.clearContent(any()) }.wasNotInvoked() - coVerify { arrangement.conversationRepository.deleteConversationLocally(any()) }.wasNotInvoked() + coVerify { arrangement.clearConversationContent(any(), eq(true)) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasInvoked(exactly = 1) } @Test - fun givenDeleteLocalConversationInvoked_whenContentClearIsUnsuccessful_thenErrorResultIsPropagated() = runTest { + fun givenDeleteLocalConversationInvoked_whenClearContentIsUnsuccessful_thenErrorResultIsPropagated() = runTest { // given val (arrangement, useCase) = Arrangement() - .withClearContent(ERROR) - .withClearLocalAsset(SUCCESS) + .withClearLocalAsset(false) .withDeleteLocalConversation(SUCCESS) .arrange() @@ -86,28 +86,8 @@ class DeleteConversationLocallyUseCaseTest { // then assertIs>(result) - coVerify { arrangement.clearLocalConversationAssets(any()) }.wasInvoked(exactly = 1) - coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = 1) - coVerify { arrangement.conversationRepository.deleteConversationLocally(any()) }.wasNotInvoked() - } - - @Test - fun givenDeleteLocalConversationInvoked_whenDeleteConversationIsUnsuccessful_thenErrorResultIsPropagated() = runTest { - // given - val (arrangement, useCase) = Arrangement() - .withClearContent(SUCCESS) - .withClearLocalAsset(SUCCESS) - .withDeleteLocalConversation(ERROR) - .arrange() - - // when - val result = useCase(CONVERSATION_ID) - - // then - assertIs>(result) - coVerify { arrangement.clearLocalConversationAssets(any()) }.wasInvoked(exactly = 1) - coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = 1) - coVerify { arrangement.conversationRepository.deleteConversationLocally(any()) }.wasInvoked(exactly = 1) + coVerify { arrangement.clearConversationContent(any(), eq(true)) }.wasInvoked(exactly = 1) + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasNotInvoked() } private class Arrangement { @@ -115,23 +95,22 @@ class DeleteConversationLocallyUseCaseTest { val conversationRepository = mock(ConversationRepository::class) @Mock - val clearLocalConversationAssets = mock(ClearConversationAssetsLocallyUseCase::class) - - suspend fun withClearContent(result: Either) = apply { - coEvery { conversationRepository.clearContent(any()) }.returns(result) - } + val clearConversationContent = mock(ClearConversationContentUseCase::class) suspend fun withDeleteLocalConversation(result: Either) = apply { - coEvery { conversationRepository.deleteConversationLocally(any()) }.returns(result) + coEvery { conversationRepository.deleteConversation(any()) }.returns(result) } - suspend fun withClearLocalAsset(result: Either) = apply { - coEvery { clearLocalConversationAssets(any()) }.returns(result) + suspend fun withClearLocalAsset(isSuccess: Boolean) = apply { + coEvery { clearConversationContent(any(), any()) }.returns( + if (isSuccess) ClearConversationContentUseCase.Result.Success + else ClearConversationContentUseCase.Result.Failure(ERROR.value) + ) } fun arrange() = this to DeleteConversationLocallyUseCaseImpl( conversationRepository = conversationRepository, - clearLocalConversationAssets = clearLocalConversationAssets + clearConversationContent = clearConversationContent ) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt index 39ce8e1d7ad..2059efcb034 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/ClearConversationContentHandlerTest.kt @@ -18,16 +18,18 @@ package com.wire.kalium.logic.sync.receiver.conversation import com.wire.kalium.logic.data.conversation.ClientId -import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.IsMessageSentInSelfConversationUseCase import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.ClearConversationAssetsLocallyUseCase import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.sync.receiver.handler.ClearConversationContentHandler import com.wire.kalium.logic.sync.receiver.handler.ClearConversationContentHandlerImpl +import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangementImpl import io.mockative.Mock import io.mockative.any import io.mockative.coEvery @@ -41,12 +43,13 @@ import kotlin.test.Test class ClearConversationContentHandlerTest { @Test - fun givenMessageFromOtherClient_whenMessageNeedsToBeRemovedLocallyAndUserIsNotPartOfConversation_thenWholeConversationShouldBeDeleted() = + fun givenMessageFromOtherUserAndNeedToRemove_whenMessageNotInSelfConversation_thenWholeConversationShouldNotBeDeleted() = runTest { // given val (arrangement, handler) = Arrangement() - .withMessageSentInSelfConversation(false) - .arrange() + .arrange { + withMessageSentInSelfConversation(false) + } // when handler.handle( @@ -59,19 +62,18 @@ class ClearConversationContentHandlerTest { ) // then - coVerify { arrangement.conversationRepository.deleteConversation(any()) } - .wasInvoked(exactly = once) - coVerify { arrangement.conversationRepository.clearContent(any()) } - .wasNotInvoked() + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = once) } @Test - fun givenMessageFromOtherClient_whenMessageNeedsToBeRemovedLocallyAndUserIsPartOfConversation_thenOnlyContentShouldBeCleared() = + fun givenMessageFromOtherClient_whenMessageInSelfConversation_thenDoNothing() = runTest { // given val (arrangement, handler) = Arrangement() - .withMessageSentInSelfConversation(true) - .arrange() + .arrange { + withMessageSentInSelfConversation(true) + } // when handler.handle( @@ -84,19 +86,18 @@ class ClearConversationContentHandlerTest { ) // then - coVerify { arrangement.conversationRepository.deleteConversation(any()) } - .wasNotInvoked() - coVerify { arrangement.conversationRepository.clearContent(any()) } - .wasInvoked(exactly = once) + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasNotInvoked() } @Test - fun givenMessageFromOtherClient_whenMessageDoesNotNeedToBeRemovedAndUserIsNotPartOfConversation_thenContentNorConversationShouldBeRemoved() = + fun givenMessageFromOtherUser_whenMessageNotInSelfConversationAndNoNeedToRemove_thenOnlyClearContent() = runTest { // given val (arrangement, handler) = Arrangement() - .withMessageSentInSelfConversation(false) - .arrange() + .arrange { + withMessageSentInSelfConversation(false) + } // when handler.handle( @@ -109,22 +110,21 @@ class ClearConversationContentHandlerTest { ) // then - coVerify { arrangement.conversationRepository.deleteConversation(any()) } - .wasNotInvoked() - coVerify { arrangement.conversationRepository.clearContent(any()) } - .wasNotInvoked() + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = once) } @Test - fun givenMessageFromOtherClient_whenMessageDoesNotNeedToBeRemovedAndUserIsPartOfConversation_thenContentShouldBeRemoved() = runTest { + fun givenMessageFromTheSelfUser_whenMessageNotInSelfConversation_thenContentNorConversationShouldBeRemoved() = runTest { // given val (arrangement, handler) = Arrangement() - .withMessageSentInSelfConversation(true) - .arrange() + .arrange { + withMessageSentInSelfConversation(false) + } // when handler.handle( - message = MESSAGE, + message = OWN_MESSAGE, messageContent = MessageContent.Cleared( conversationId = CONVERSATION_ID, time = Instant.DISTANT_PAST, @@ -133,18 +133,67 @@ class ClearConversationContentHandlerTest { ) // then - coVerify { arrangement.conversationRepository.deleteConversation(any()) } - .wasNotInvoked() - coVerify { arrangement.conversationRepository.clearContent(any()) } - .wasInvoked(exactly = once) + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasNotInvoked() + } + + @Test + fun givenSelfSenderAndMessageInSelfConversation_whenNeedToRemoveAndConversationIsNotLeftYet_thenContentCleared() = runTest { + // given + val (arrangement, handler) = Arrangement() + .arrange { + withMessageSentInSelfConversation(true) + withGetConversationMembers(listOf(TestUser.USER_ID)) + } + + // when + handler.handle( + message = OWN_MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = true + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = once) + coVerify { arrangement.conversationRepository.addConversationToDeleteQueue(any()) }.wasInvoked(exactly = once) } @Test - fun givenMessageFromTheSameClient_whenHandleIsInvoked_thenContentNorConversationShouldBeRemoved() = runTest { + fun givenSelfSenderAndMessageInSelfConversation_whenNeedToRemoveAndLeftConversation_thenContentAndConversationRemoved() = runTest { // given val (arrangement, handler) = Arrangement() - .withMessageSentInSelfConversation(true) - .arrange() + .arrange { + withMessageSentInSelfConversation(true) + withGetConversationMembers(listOf()) + } + + // when + handler.handle( + message = OWN_MESSAGE, + messageContent = MessageContent.Cleared( + conversationId = CONVERSATION_ID, + time = Instant.DISTANT_PAST, + needToRemoveLocally = true + ) + ) + + // then + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasInvoked(exactly = once) + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = once) + coVerify { arrangement.conversationRepository.addConversationToDeleteQueue(any()) }.wasNotInvoked() + } + + @Test + fun givenMessageFromTheSelfUser_whenMessageInSelfConversationAndNoNeedToRemove_thenOnlyContentRemoved() = runTest { + // given + val (arrangement, handler) = Arrangement() + .arrange { + withMessageSentInSelfConversation(true) + } // when handler.handle( @@ -157,33 +206,36 @@ class ClearConversationContentHandlerTest { ) // then - coVerify { arrangement.conversationRepository.deleteConversation(any()) } - .wasNotInvoked() - coVerify { arrangement.conversationRepository.clearContent(any()) } - .wasNotInvoked() + coVerify { arrangement.conversationRepository.deleteConversation(any()) }.wasNotInvoked() + coVerify { arrangement.conversationRepository.clearContent(any()) }.wasInvoked(exactly = once) } - private class Arrangement { + private class Arrangement : ConversationRepositoryArrangement by ConversationRepositoryArrangementImpl() { @Mock - val conversationRepository = mock(ConversationRepository::class) + val isMessageSentInSelfConversationUseCase = mock(IsMessageSentInSelfConversationUseCase::class) @Mock - val isMessageSentInSelfConversationUseCase = mock(IsMessageSentInSelfConversationUseCase::class) + val clearConversationAssetsLocally = mock(ClearConversationAssetsLocallyUseCase::class) suspend fun withMessageSentInSelfConversation(isSentInSelfConv: Boolean) = apply { coEvery { isMessageSentInSelfConversationUseCase(any()) }.returns(isSentInSelfConv) } - suspend fun arrange(): Pair = - this to ClearConversationContentHandlerImpl( + suspend fun arrange(block: suspend Arrangement.() -> Unit): Pair = run { + val clearConversationContentHandler = ClearConversationContentHandlerImpl( conversationRepository = conversationRepository, selfUserId = TestUser.USER_ID, isMessageSentInSelfConversation = isMessageSentInSelfConversationUseCase, - ).apply { - coEvery { conversationRepository.deleteConversation(any()) }.returns(Either.Right(Unit)) - coEvery { conversationRepository.clearContent(any()) }.returns(Either.Right(Unit)) - } + clearLocalConversationAssets = clearConversationAssetsLocally + ) + withDeletingConversationSucceeding() + withClearContentSucceeding() + coEvery { clearConversationAssetsLocally(any()) }.returns(Either.Right(Unit)) + block() + + this to clearConversationContentHandler + } } companion object { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandlerTest.kt index ef81ba0270c..542b394769d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandlerTest.kt @@ -33,6 +33,10 @@ import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangement import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangementImpl import com.wire.kalium.logic.util.arrangement.provider.SelfTeamIdProviderArrangement import com.wire.kalium.logic.util.arrangement.provider.SelfTeamIdProviderArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.ConnectionRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.ConnectionRepositoryArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.usecase.PersistMessageUseCaseArrangement @@ -71,7 +75,7 @@ class MemberLeaveEventHandlerTest { ) } - memberLeaveEventHandler.handle(memberLeaveEvent(reason = MemberLeaveReason.Left)) + memberLeaveEventHandler.handle(event) coVerify { arrangement.memberDAO.deleteMembersByQualifiedID(list, qualifiedConversationIdEntity) @@ -101,7 +105,7 @@ class MemberLeaveEventHandlerTest { withDeleteMembersByQualifiedIDThrows(throws = IllegalArgumentException()) } - memberLeaveEventHandler.handle(memberLeaveEvent(reason = MemberLeaveReason.Left)) + memberLeaveEventHandler.handle(event) coVerify { arrangement.memberDAO.deleteMembersByQualifiedID(list, qualifiedConversationIdEntity) @@ -131,7 +135,7 @@ class MemberLeaveEventHandlerTest { withIsAtLeastOneUserATeamMember(Either.Right(true)) } - memberLeaveEventHandler.handle(memberLeaveEvent(reason = MemberLeaveReason.UserDeleted)) + memberLeaveEventHandler.handle(event) coVerify { arrangement.userRepository.fetchUsersIfUnknownByIds(event.removedList.toSet()) @@ -162,7 +166,7 @@ class MemberLeaveEventHandlerTest { withPersistingMessage(Either.Right(Unit)) } - memberLeaveEventHandler.handle(memberLeaveEvent(reason = MemberLeaveReason.UserDeleted)) + memberLeaveEventHandler.handle(event) coVerify { arrangement.userRepository.fetchUsersIfUnknownByIds(event.removedList.toSet()) @@ -251,11 +255,40 @@ class MemberLeaveEventHandlerTest { }.wasInvoked(exactly = once) } + @Test + fun givenDaoReturnsSuccessAndConversationInDeleteQueue_whenDeletingSelfMember_thenConversationDeleted() = runTest { + val event = memberLeaveEvent(reason = MemberLeaveReason.Left).copy(removedList = listOf(selfUserId), removedBy = selfUserId) + + val (arrangement, memberLeaveEventHandler) = Arrangement() + .arrange { + withDeleteMembersByQualifiedID( + result = event.removedList.size.toLong(), + conversationId = EqualsMatcher(event.conversationId.toDao()), + memberIdList = EqualsMatcher(event.removedList.map { QualifiedIDEntity(it.value, it.domain) }) + ) + withFetchUsersIfUnknownByIdsReturning(Either.Right(Unit), userIdList = EqualsMatcher(event.removedList.toSet())) + withTeamId(Either.Right(null)) + withPersistingMessage(Either.Right(Unit)) + withGetConversationsDeleteQueue(listOf(event.conversationId)) + withDeletingConversationSucceeding(EqualsMatcher(event.conversationId)) + } + + memberLeaveEventHandler.handle(event) + + coVerify { + arrangement.updateConversationClientsForCurrentCall.invoke(eq(event.conversationId)) + }.wasInvoked(exactly = once) + coVerify { arrangement.conversationRepository.getConversationsDeleteQueue() }.wasInvoked(once) + coVerify { arrangement.conversationRepository.deleteConversation(event.conversationId) }.wasInvoked(once) + coVerify { arrangement.conversationRepository.removeConversationFromDeleteQueue(event.conversationId) }.wasInvoked(once) + } + private class Arrangement : UserRepositoryArrangement by UserRepositoryArrangementImpl(), PersistMessageUseCaseArrangement by PersistMessageUseCaseArrangementImpl(), MemberDAOArrangement by MemberDAOArrangementImpl(), - SelfTeamIdProviderArrangement by SelfTeamIdProviderArrangementImpl() { + SelfTeamIdProviderArrangement by SelfTeamIdProviderArrangementImpl(), + ConversationRepositoryArrangement by ConversationRepositoryArrangementImpl() { @Mock val updateConversationClientsForCurrentCall = mock(UpdateConversationClientsForCurrentCallUseCase::class) @@ -269,14 +302,17 @@ class MemberLeaveEventHandlerTest { coEvery { legalHoldHandler.handleConversationMembersChanged(any()) }.returns(Either.Right(Unit)) + withRemoveConversationToDeleteQueue() block() memberLeaveEventHandler = MemberLeaveEventHandlerImpl( memberDAO = memberDAO, userRepository = userRepository, + conversationRepository = conversationRepository, persistMessage = persistMessageUseCase, updateConversationClientsForCurrentCall = lazy { updateConversationClientsForCurrentCall }, legalHoldHandler = legalHoldHandler, - selfTeamIdProvider = selfTeamIdProvider + selfTeamIdProvider = selfTeamIdProvider, + selfUserId = selfUserId ) this to memberLeaveEventHandler } @@ -284,6 +320,7 @@ class MemberLeaveEventHandlerTest { companion object { val failure = CoreFailure.MissingClientRegistration + val selfUserId = UserId("self-userId", "domain") val userId = UserId("userId", "domain") private val qualifiedUserIdEntity = QualifiedIDEntity("userId", "domain") private val qualifiedConversationIdEntity = QualifiedIDEntity("conversationId", "domain") diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ConversationRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ConversationRepositoryArrangement.kt index 1a223b651d9..16d51fec013 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ConversationRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ConversationRepositoryArrangement.kt @@ -111,6 +111,11 @@ internal interface ConversationRepositoryArrangement { suspend fun withConversationDetailsByIdReturning(result: Either) suspend fun withPersistMembers(result: Either) suspend fun withMembersNameAndHandle(result: Either>) + suspend fun withAddConversationToDeleteQueue() + suspend fun withRemoveConversationToDeleteQueue() + suspend fun withGetConversationsDeleteQueue(result: List) + suspend fun withClearContentSucceeding() + suspend fun withGetConversationMembers(result: List) } internal open class ConversationRepositoryArrangementImpl : ConversationRepositoryArrangement { @@ -272,4 +277,24 @@ internal open class ConversationRepositoryArrangementImpl : ConversationReposito override suspend fun withMembersNameAndHandle(result: Either>) { coEvery { conversationRepository.selectMembersNameAndHandle(any()) }.returns(result) } + + override suspend fun withAddConversationToDeleteQueue() { + coEvery { conversationRepository.addConversationToDeleteQueue(any()) }.returns(Unit) + } + + override suspend fun withRemoveConversationToDeleteQueue() { + coEvery { conversationRepository.removeConversationFromDeleteQueue(any()) }.returns(Unit) + } + + override suspend fun withGetConversationsDeleteQueue(result: List) { + coEvery { conversationRepository.getConversationsDeleteQueue() }.returns(result) + } + + override suspend fun withClearContentSucceeding() { + coEvery { conversationRepository.clearContent(any()) }.returns(Either.Right(Unit)) + } + + override suspend fun withGetConversationMembers(result: List) { + coEvery { conversationRepository.getConversationMembers(any()) }.returns(Either.Right(result)) + } } From f0ce09ae140e2a7ce27af364c4884f4e07b54e08 Mon Sep 17 00:00:00 2001 From: Boris Safonov Date: Mon, 20 Jan 2025 12:28:10 +0200 Subject: [PATCH 8/9] Code style fixes --- .../kalium/logic/data/conversation/ConversationRepository.kt | 1 - .../logic/sync/receiver/conversation/MemberLeaveEventHandler.kt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) 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 e5154518256..d2b30926c21 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 @@ -67,7 +67,6 @@ import com.wire.kalium.network.api.authenticated.conversation.model.Conversation import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import com.wire.kalium.persistence.dao.MetadataDAO -import com.wire.kalium.persistence.dao.MetadataDAOImpl import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.client.ClientDAO import com.wire.kalium.persistence.dao.conversation.ConversationDAO diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt index a6686b31c6a..a6c4f2d392f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/MemberLeaveEventHandler.kt @@ -47,6 +47,7 @@ interface MemberLeaveEventHandler { suspend fun handle(event: Event.Conversation.MemberLeave): Either } +@Suppress("LongParameterList") internal class MemberLeaveEventHandlerImpl( private val memberDAO: MemberDAO, private val userRepository: UserRepository, From 45e222a8e82eb0755c003fa8ca02d0ad3da06145 Mon Sep 17 00:00:00 2001 From: Boris Safonov Date: Thu, 23 Jan 2025 17:02:37 +0200 Subject: [PATCH 9/9] Updated github action upload-artifac to v4 --- .github/workflows/gradle-android-instrumented-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle-android-instrumented-tests.yml b/.github/workflows/gradle-android-instrumented-tests.yml index 434a797d9b9..6eb1a28c0a7 100644 --- a/.github/workflows/gradle-android-instrumented-tests.yml +++ b/.github/workflows/gradle-android-instrumented-tests.yml @@ -89,14 +89,14 @@ jobs: - name: Archive Test Reports if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-reports path: ./**/build/reports/tests/** - name: Archive Test Results if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results path: |