From f14fc7a1ae7e7637f6d8ca88d336134da02acf5a Mon Sep 17 00:00:00 2001 From: Yamil Medina Date: Wed, 9 Mar 2022 08:38:02 -0300 Subject: [PATCH] feat: add use case to download assets by its asset key/id (AR-1094) (#243) * feat: create basic structure to update self user, calling api endpoint * feat: map user data from api and persist into db entity * feat: uploaded data and linking to user self * chore: fix tests and configure primiteve adapter for native sq to kotlin types * chore: fix tests for uploadavatar case, mocks * refactor: rename of user on persistence module to userentity agreed suffix * fix: persiste userentity on remote update success * fix: add comment about not mapped field yet * fix: wip model asset entity * feat: add mapping and dao to persist an asset * feat: map and store the uploaded asset into the table * test: fix broken test * feat: add mapping for FKs and define strategy to fetch/download pics * feat: add logic to persist assets from users response and also after uploading an asset * feat: change strategy for fk on users, assets * test: fix tests as vargars was causing issues with mockative * chore: add todo task to be refined later * chore: fix reference on cli proj * chore: fix tests on network module * feat: add usecase base call to download a public asset * feature: add map of pictures connections users * feature: refactor map func * feature: add map of pictures connections users * feature: refactor map func * feature: rename asset usecase for public assets * test: fix broken test * fix: refactor code to use wrapApiRequest * fix: fix cli module ref * chore: add test coverage for assetsrepository * chore: refactor naming of functions and add func to persist asset data * chore: address pr comments, remove md5 field and calulate it * chore: address pr comments, remove field from db * chore: reference on imports fixed * chore: fix test reference * chore: fix test reference * chore: add tests for assetrepository with new structure * chore: fix returns value from usecase * chore: refactor asset table to have minimal fields, metadata will be needed for assetmsgs * chore: rename mappers functions and todo added * chore: refactor, apply changes to sync contacts pics on one step donwload * chore: make rawdata notnullable * chore: change input and return of usecase to not expose either * chore: fix pr comments, applying better naming * chore: fix test ref * chore: fix broken test * chore: remove hard ref FK on users/assets to allow sync contacts on demand download * chore: add cause to failure result on usecases for avatar * fix: returns uploaded assetid in usecase * enhancement: adds documentation to public interfaces --- .../wire/kalium/presentation/MainActivity.kt | 38 +++++- .../kalium/logic/data/asset/AssetMapper.kt | 16 ++- .../logic/data/asset/AssetRepository.kt | 41 ++++-- .../wire/kalium/logic/data/user/UserModel.kt | 6 +- .../kalium/logic/data/user/UserRepository.kt | 26 +--- .../feature/asset/GetPublicAssetUseCase.kt | 32 +++++ .../feature/user/UploadUserAvatarUseCase.kt | 15 +- .../kalium/logic/feature/user/UserScope.kt | 3 + .../logic/data/asset/AssetRepositoryTest.kt | 128 +++++++++++++++++- .../asset/GetPublicAssetUseCaseTest.kt | 71 ++++++++++ .../user/UploadUserAvatarUseCaseTest.kt | 22 ++- .../wire/kalium/persistence/dao/UserDAO.kt | 6 +- .../kalium/persistence/dao/asset/AssetDAO.kt | 4 + .../persistence/dao/asset/AssetDAOImpl.kt | 19 +++ .../com/wire/kalium/persistence/db/Assets.sq | 2 - .../com/wire/kalium/persistence/db/Users.sq | 4 +- 16 files changed, 373 insertions(+), 60 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/GetPublicAssetUseCase.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/GetPublicAssetUseCaseTest.kt diff --git a/android/src/main/kotlin/com/wire/kalium/presentation/MainActivity.kt b/android/src/main/kotlin/com/wire/kalium/presentation/MainActivity.kt index 720edda7097..a6c4429e32a 100644 --- a/android/src/main/kotlin/com/wire/kalium/presentation/MainActivity.kt +++ b/android/src/main/kotlin/com/wire/kalium/presentation/MainActivity.kt @@ -1,17 +1,26 @@ package com.wire.kalium.presentation +import android.graphics.BitmapFactory import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.Divider import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.unit.dp import androidx.lifecycle.lifecycleScope import com.wire.kalium.KaliumApplication import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.ServerConfig import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.user.SelfUser +import com.wire.kalium.logic.feature.asset.PublicAssetResult import com.wire.kalium.logic.feature.auth.AuthSession import com.wire.kalium.logic.feature.auth.AuthenticationResult import com.wire.kalium.logic.feature.auth.AuthenticationScope @@ -31,10 +40,20 @@ class MainActivity : ComponentActivity() { login(coreLogic.getAuthenticationScope())?.let { val session = coreLogic.getSessionScope(it) val conversations = session.conversations.getConversations().first() + + // Uploading image code +// val imageContent = applicationContext.assets.open("moon1.jpg").readBytes() +// session.users.uploadUserAvatar("image/jpg", imageContent) + val selfUser = session.users.getSelfUser().first() + val avatarAsset = when (val publicAsset = session.users.getPublicAsset(selfUser.previewPicture.toString())) { + is PublicAssetResult.Success -> publicAsset.asset + else -> null + } + setContent { - MainLayout(conversations, selfUser) + MainLayout(conversations, selfUser, avatarAsset) } } } @@ -54,11 +73,26 @@ class MainActivity : ComponentActivity() { } @Composable -fun MainLayout(conversations: List, selfUser: SelfUser) { +fun MainLayout(conversations: List, selfUser: SelfUser, avatarAsset: ByteArray?) { Column { Text("Conversation count:") Text("${conversations.size}") Text("SelfUser:") Text("$selfUser") + + Divider( + modifier = Modifier.fillMaxWidth(), + thickness = 0.5.dp + ) + + Text(text = "Avatar picture:") + + avatarAsset?.let { byteArray -> + Image( + bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)?.asImageBitmap()!!, + contentDescription = "", + modifier = Modifier.size(300.dp) + ) + } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetMapper.kt index f8c28143383..78278e5a678 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetMapper.kt @@ -1,19 +1,17 @@ package com.wire.kalium.logic.data.asset import com.wire.kalium.cryptography.utils.calcMd5 -import com.wire.kalium.logic.data.user.UserAssetId import com.wire.kalium.network.api.asset.AssetMetadataRequest import com.wire.kalium.network.api.asset.AssetResponse import com.wire.kalium.network.api.model.AssetRetentionType import com.wire.kalium.persistence.dao.asset.AssetEntity -import io.ktor.utils.io.core.toByteArray import kotlinx.datetime.Clock interface AssetMapper { fun toMetadataApiModel(uploadAssetMetadata: UploadAssetData): AssetMetadataRequest fun fromApiUploadResponseToDomainModel(asset: AssetResponse): UploadedAssetId fun fromUploadedAssetToDaoModel(uploadAssetData: UploadAssetData, uploadedAssetResponse: AssetResponse): AssetEntity - fun fromUserAssetIdToDaoModel(assetId: UserAssetId): AssetEntity + fun fromUserAssetToDaoModel(assetKey: String, data: ByteArray): AssetEntity } class AssetMapperImpl : AssetMapper { @@ -34,12 +32,18 @@ class AssetMapperImpl : AssetMapper { key = uploadedAssetResponse.key, domain = uploadedAssetResponse.domain, mimeType = uploadAssetData.mimeType.name, - rawData = uploadAssetData.data, // should use something like byteArray to encrypt aes256cbc + rawData = uploadAssetData.data, downloadedDate = Clock.System.now().toEpochMilliseconds() ) } - override fun fromUserAssetIdToDaoModel(assetId: UserAssetId): AssetEntity { - return AssetEntity(assetId.toString(), "", null, "".toByteArray(), null) + override fun fromUserAssetToDaoModel(assetKey: String, data: ByteArray): AssetEntity { + return AssetEntity( + key = assetKey, + domain = "", // is it possible to know this on contacts sync avatars ? + mimeType = "", + rawData = data, + downloadedDate = Clock.System.now().toEpochMilliseconds() + ) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt index 667b8ed7b52..6a873313742 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt @@ -8,10 +8,12 @@ import com.wire.kalium.logic.functional.suspending import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.network.api.asset.AssetApi import com.wire.kalium.persistence.dao.asset.AssetDAO +import kotlinx.coroutines.flow.firstOrNull interface AssetRepository { suspend fun uploadAndPersistPublicAsset(uploadAssetData: UploadAssetData): Either - suspend fun saveUserPictureAsset(assetId: List): Either + suspend fun downloadPublicAsset(assetKey: String): Either + suspend fun downloadUsersPictureAssets(assetId: List): Either } internal class AssetDataSource( @@ -20,21 +22,36 @@ internal class AssetDataSource( private val assetDao: AssetDAO ) : AssetRepository { - override suspend fun uploadAndPersistPublicAsset(uploadAssetData: UploadAssetData): Either = suspending { - wrapApiRequest { - assetMapper.toMetadataApiModel(uploadAssetData).let { metaData -> - assetApi.uploadAsset(metaData, uploadAssetData.data) + override suspend fun uploadAndPersistPublicAsset(uploadAssetData: UploadAssetData): Either = + suspending { + wrapApiRequest { + // we should also consider for images, the compression for preview vs complete picture + assetMapper.toMetadataApiModel(uploadAssetData).let { metaData -> + assetApi.uploadAsset(metaData, uploadAssetData.data) + } + }.map { assetResponse -> + val assetEntity = assetMapper.fromUploadedAssetToDaoModel(uploadAssetData, assetResponse) + assetDao.insertAsset(assetEntity) + assetMapper.fromApiUploadResponseToDomainModel(assetResponse) } - }.map { assetResponse -> - val assetEntity = assetMapper.fromUploadedAssetToDaoModel(uploadAssetData, assetResponse) - assetDao.insertAsset(assetEntity) - assetMapper.fromApiUploadResponseToDomainModel(assetResponse) + } + + override suspend fun downloadPublicAsset(assetKey: String): Either = suspending { + val persistedAsset = assetDao.getAssetByKey(assetKey).firstOrNull() + if (persistedAsset != null) return@suspending Either.Right(persistedAsset.rawData) + + wrapApiRequest { + assetApi.downloadAsset(assetKey, null) + }.map { assetData -> + assetDao.insertAsset(assetMapper.fromUserAssetToDaoModel(assetKey, assetData)) + assetData } } - override suspend fun saveUserPictureAsset(assetId: List): Either = suspending { - // TODO: on next PR we should download immediately the asset data and persist it - assetDao.insertAssets(assetId.map { assetMapper.fromUserAssetIdToDaoModel(it) }) + override suspend fun downloadUsersPictureAssets(assetId: List): Either = suspending { + assetId.filterNotNull().forEach { + downloadPublicAsset(it) + } return@suspending Either.Right(Unit) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt index 550224e9312..d5f1765a9da 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt @@ -16,8 +16,8 @@ data class SelfUser( val phone: String?, val accentId: Int, val team: String?, - val previewPicture: UserAssetId, - val completePicture: UserAssetId + val previewPicture: UserAssetId?, + val completePicture: UserAssetId? ) : User() -typealias UserAssetId = String? +typealias UserAssetId = String 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 c31dfa8152d..c4606a1c142 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 @@ -13,7 +13,6 @@ import com.wire.kalium.network.api.user.self.SelfApi import com.wire.kalium.network.utils.isSuccessful import com.wire.kalium.persistence.dao.MetadataDAO import com.wire.kalium.persistence.dao.UserDAO -import com.wire.kalium.persistence.dao.UserEntity import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -30,7 +29,7 @@ interface UserRepository { suspend fun fetchKnownUsers(): Either suspend fun fetchUsersByIds(ids: Set): Either suspend fun getSelfUser(): Flow - suspend fun updateSelfUser(newName: String? = null, newAccent: Int? = null, newAssetId: String? = null): Either + suspend fun updateSelfUser(newName: String? = null, newAccent: Int? = null, newAssetId: String? = null): Either } class UserDataSource( @@ -49,7 +48,7 @@ class UserDataSource( }.coFold({ Either.Left(it) }, { user -> - assetRepository.saveUserPictureAsset(listOf(user.previewAssetId, user.completeAssetId)) + assetRepository.downloadUsersPictureAssets(listOf(user.previewAssetId, user.completeAssetId)) userDAO.insertUser(user) metadataDAO.insertValue(Json.encodeToString(user.id), SELF_USER_ID_KEY) Either.Right(Unit) @@ -70,10 +69,7 @@ class UserDataSource( }.coFold({ Either.Left(it) }, { - val usersToBePersisted = it.map(userMapper::fromApiModelToDaoModel) - // Save (in case there is no data) a reference to the asset id (profile picture) - assetRepository.saveUserPictureAsset(mapAssetsForUsersToBePersisted(usersToBePersisted)) - userDAO.insertUsers(usersToBePersisted) + userDAO.insertUsers(it.map(userMapper::fromApiModelToDaoModel)) Either.Right(Unit) }) } @@ -88,16 +84,7 @@ class UserDataSource( } } - private fun mapAssetsForUsersToBePersisted(usersToBePersisted: List): List { - val assetsId = mutableListOf() - usersToBePersisted.map { - assetsId.add(it.completeAssetId) - assetsId.add(it.previewAssetId) - } - return assetsId - } - - override suspend fun updateSelfUser(newName: String?, newAccent: Int?, newAssetId: String?): Either { + override suspend fun updateSelfUser(newName: String?, newAccent: Int?, newAssetId: String?): Either { val user = getSelfUser().firstOrNull() ?: return Either.Left(CoreFailure.Unknown(NullPointerException())) @@ -105,8 +92,9 @@ class UserDataSource( val updatedSelf = selfApi.updateSelf(updateRequest) return if (updatedSelf.isSuccessful()) { - userDAO.updateUser(userMapper.fromUpdateRequestToDaoModel(user, updateRequest)) - Either.Right(Unit) + val updatedUser = userMapper.fromUpdateRequestToDaoModel(user, updateRequest) + userDAO.updateUser(updatedUser) + Either.Right(userMapper.fromDaoModel(updatedUser)) } else { Either.Left(CoreFailure.Unknown(IllegalStateException())) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/GetPublicAssetUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/GetPublicAssetUseCase.kt new file mode 100644 index 00000000000..dcac9d3354f --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/GetPublicAssetUseCase.kt @@ -0,0 +1,32 @@ +package com.wire.kalium.logic.feature.asset + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.asset.AssetRepository +import com.wire.kalium.logic.data.user.UserAssetId +import com.wire.kalium.logic.functional.suspending + +interface GetPublicAssetUseCase { + /** + * Function that enables downloading a public asset by its asset-key, mostly used for avatar pictures + * Internally, if the asset doesn't exist locally, this will download it first and then return it + * + * @param assetKey the asset identifier + * @return PublicAssetResult with a [ByteArray] in case of success or [CoreFailure] on failure + */ + suspend operator fun invoke(assetKey: UserAssetId): PublicAssetResult +} + +internal class GetPublicAssetUseCaseImpl(private val assetDataSource: AssetRepository) : GetPublicAssetUseCase { + override suspend fun invoke(assetKey: UserAssetId): PublicAssetResult = suspending { + assetDataSource.downloadPublicAsset(assetKey).fold({ + PublicAssetResult.Failure(it) + }) { + PublicAssetResult.Success(it) + } + } +} + +sealed class PublicAssetResult { + class Success(val asset: ByteArray) : PublicAssetResult() + class Failure(val coreFailure: CoreFailure) : PublicAssetResult() +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt index e596e1f0aec..5976fbfef9e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt @@ -1,9 +1,11 @@ package com.wire.kalium.logic.feature.user +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.asset.ImageAsset import com.wire.kalium.logic.data.asset.RetentionType import com.wire.kalium.logic.data.asset.UploadAssetData +import com.wire.kalium.logic.data.user.UserAssetId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.functional.suspending @@ -14,11 +16,12 @@ interface UploadUserAvatarUseCase { * * @param mimeType mimetype of the user picture * @param imageData binary data of the actual picture + * @return UploadAvatarResult with [UserAssetId] in case of success or [CoreFailure] on failure */ suspend operator fun invoke(mimeType: String, imageData: ByteArray): UploadAvatarResult } -class UploadUserAvatarUseCaseImpl( +internal class UploadUserAvatarUseCaseImpl( private val userDataSource: UserRepository, private val assetDataSource: AssetRepository ) : UploadUserAvatarUseCase { @@ -29,14 +32,14 @@ class UploadUserAvatarUseCaseImpl( .flatMap { asset -> userDataSource.updateSelfUser(newAssetId = asset.key) }.fold({ - UploadAvatarResult.Failure - }) { - UploadAvatarResult.Success + UploadAvatarResult.Failure(it) + }) { updatedUser -> + UploadAvatarResult.Success(updatedUser.completePicture!!) } // todo: remove old assets, non blocking this response, as will imply deleting locally and remotely } } sealed class UploadAvatarResult { - object Success : UploadAvatarResult() - object Failure : UploadAvatarResult() + class Success(val userAssetId: UserAssetId) : UploadAvatarResult() + class Failure(val coreFailure: CoreFailure) : UploadAvatarResult() } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt index a003d337d32..dccffe2fe9e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt @@ -2,6 +2,8 @@ package com.wire.kalium.logic.feature.user import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.feature.asset.GetPublicAssetUseCaseImpl +import com.wire.kalium.logic.feature.asset.GetPublicAssetUseCase import com.wire.kalium.logic.sync.SyncManager class UserScope( @@ -13,4 +15,5 @@ class UserScope( val syncSelfUser: SyncSelfUserUseCase get() = SyncSelfUserUseCase(userRepository) val syncContacts: SyncContactsUseCase get() = SyncContactsUseCaseImpl(userRepository) val uploadUserAvatar: UploadUserAvatarUseCase get() = UploadUserAvatarUseCaseImpl(userRepository, assetRepository) + val getPublicAsset: GetPublicAssetUseCase get() = GetPublicAssetUseCaseImpl(assetRepository) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/asset/AssetRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/asset/AssetRepositoryTest.kt index 29dbaa68bb3..c81a6808300 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/asset/AssetRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/asset/AssetRepositoryTest.kt @@ -1,6 +1,7 @@ package com.wire.kalium.logic.data.asset import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.user.UserAssetId import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed import com.wire.kalium.network.api.ErrorResponse @@ -9,14 +10,19 @@ import com.wire.kalium.network.api.asset.AssetResponse import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.persistence.dao.asset.AssetDAO +import com.wire.kalium.persistence.dao.asset.AssetEntity +import io.ktor.utils.io.core.toByteArray import io.mockative.Mock import io.mockative.any import io.mockative.classOf +import io.mockative.eq import io.mockative.given import io.mockative.mock import io.mockative.once import io.mockative.thenDoNothing +import io.mockative.twice import io.mockative.verify +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -32,9 +38,11 @@ class AssetRepositoryTest { private lateinit var assetRepository: AssetRepository + private val assetMapper by lazy { AssetMapperImpl() } + @BeforeTest fun setUp() { - assetRepository = AssetDataSource(assetApi, AssetMapperImpl(), assetDAO) + assetRepository = AssetDataSource(assetApi, assetMapper, assetDAO) } @Test @@ -87,4 +95,122 @@ class AssetRepositoryTest { .with(any(), any()) .wasInvoked(exactly = once) } + + @Test + fun givenAListOfAssets_whenSavingAssets_thenShouldSucceed() = runTest { + val assetsIdToPersist = listOf("assetId1", "assetId2") + val expectedImage = "my_image_asset".toByteArray() + + assetsIdToPersist.forEach { assetKey -> + mockAssetDaoGetByKeyCall(assetKey, null) + given(assetApi) + .suspendFunction(assetApi::downloadAsset) + .whenInvokedWith(eq(assetKey), eq(null)) + .thenReturn(NetworkResponse.Success(expectedImage, mapOf(), 200)) + + given(assetDAO) + .suspendFunction(assetDAO::insertAsset) + .whenInvokedWith(any()) + .thenDoNothing() + } + + val actual = assetRepository.downloadUsersPictureAssets(assetsIdToPersist) + + actual.shouldSucceed { + assertEquals(it, Unit) + } + + verify(assetDAO).suspendFunction(assetDAO::insertAsset) + .with(any()) + .wasInvoked(exactly = twice) + } + + @Test + fun givenAnAssetId_whenDownloadingAssetsAndNotPresentInDB_thenShouldReturnItsBinaryDataFromRemoteAndPersistIt() = runTest { + val assetKey = "1-3-an-asset-key" + val expectedImage = "my_image_asset".toByteArray() + + mockAssetDaoGetByKeyCall(assetKey, null) + given(assetApi) + .suspendFunction(assetApi::downloadAsset) + .whenInvokedWith(eq(assetKey), eq(null)) + .thenReturn(NetworkResponse.Success(expectedImage, mapOf(), 200)) + given(assetDAO) + .suspendFunction(assetDAO::insertAsset) + .whenInvokedWith(any()) + .thenReturn(Unit) + + val actual = assetRepository.downloadPublicAsset(assetKey) + + actual.shouldSucceed { + assertEquals(it, expectedImage) + } + + verify(assetDAO).suspendFunction(assetDAO::getAssetByKey) + .with(eq(assetKey)) + .wasInvoked(exactly = once) + verify(assetApi).suspendFunction(assetApi::downloadAsset) + .with(eq(assetKey), eq(null)) + .wasInvoked(exactly = once) + verify(assetDAO) + .suspendFunction(assetDAO::insertAsset) + .with(any()) + .wasInvoked(exactly = once) + } + + @Test + fun givenAnError_whenDownloadingAssets_thenShouldReturnThrowNetworkFailure() = runTest { + val assetKey = "1-3-an-asset-key" + val expectedImage = "my_image_asset".toByteArray() + mockAssetDaoGetByKeyCall(assetKey, null) + given(assetApi) + .suspendFunction(assetApi::downloadAsset) + .whenInvokedWith(eq(assetKey), eq(null)) + .thenReturn(NetworkResponse.Error(KaliumException.ServerError(ErrorResponse(500, "error_message", "error_label")))) + + val actual = assetRepository.downloadPublicAsset(assetKey) + + actual.shouldFail { + assertEquals(it::class, NetworkFailure.ServerMiscommunication::class) + } + + verify(assetDAO).suspendFunction(assetDAO::getAssetByKey) + .with(eq(assetKey)) + .wasInvoked(exactly = once) + + verify(assetApi).suspendFunction(assetApi::downloadAsset) + .with(eq(assetKey), eq(null)) + .wasInvoked(exactly = once) + } + + @Test + fun givenAnAssetId_whenAssetIsAlreadyDownloaded_thenShouldReturnItsBinaryDataFromDB() = runTest { + val assetKey = "1-3-an-asset-key" + val expectedImage = "my_image_asset".toByteArray() + mockAssetDaoGetByKeyCall(assetKey, stubAssetEntity(assetKey, expectedImage)) + + val actual = assetRepository.downloadPublicAsset(assetKey) + + actual.shouldSucceed { + assertEquals(expectedImage, it) + } + + verify(assetDAO).suspendFunction(assetDAO::getAssetByKey) + .with(eq(assetKey)) + .wasInvoked(exactly = once) + + verify(assetApi).suspendFunction(assetApi::downloadAsset) + .with(eq(assetKey), eq(null)) + .wasNotInvoked() + } + + private fun mockAssetDaoGetByKeyCall(assetKey: String, expectedAssetEntity: AssetEntity?) { + given(assetDAO) + .suspendFunction(assetDAO::getAssetByKey) + .whenInvokedWith(eq(assetKey)) + .thenReturn(flowOf(expectedAssetEntity)) + } + + private fun stubAssetEntity(assetKey: String, rawData: ByteArray) = + AssetEntity(assetKey, "some_domain", null, rawData, 1) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/GetPublicAssetUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/GetPublicAssetUseCaseTest.kt new file mode 100644 index 00000000000..45f5d49786b --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/GetPublicAssetUseCaseTest.kt @@ -0,0 +1,71 @@ +package com.wire.kalium.logic.feature.asset + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.asset.AssetRepository +import com.wire.kalium.logic.functional.Either +import io.ktor.utils.io.core.toByteArray +import io.mockative.Mock +import io.mockative.classOf +import io.mockative.eq +import io.mockative.given +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class GetPublicAssetUseCaseTest { + + @Mock + private val assetRepository = mock(classOf()) + + private lateinit var getPublicAsset: GetPublicAssetUseCase + + @BeforeTest + fun setUp() { + getPublicAsset = GetPublicAssetUseCaseImpl(assetRepository) + } + + @Test + fun givenACallToGetAPublicAsset_whenEverythingGoesWell_thenShouldReturnsASuccessResultWithData() = runTest { + val assetKey = "some_key" + val expectedData = "A".toByteArray() + + given(assetRepository) + .suspendFunction(assetRepository::downloadPublicAsset) + .whenInvokedWith(eq(assetKey)) + .thenReturn(Either.Right(expectedData)) + + val publicAsset = getPublicAsset(assetKey) + + assertEquals(PublicAssetResult.Success::class, publicAsset::class) + assertEquals(expectedData, (publicAsset as PublicAssetResult.Success).asset) + + verify(assetRepository) + .suspendFunction(assetRepository::downloadPublicAsset) + .with(eq(assetKey)) + .wasInvoked(exactly = once) + } + + @Test + fun givenACallToGetAPublicAsset_whenEverythingThereIsAnError_thenShouldReturnsAFailureResult() = runTest { + val assetKey = "some_key" + + given(assetRepository) + .suspendFunction(assetRepository::downloadPublicAsset) + .whenInvokedWith(eq(assetKey)) + .thenReturn(Either.Left(CoreFailure.Unknown(Throwable("an error")))) + + val publicAsset = getPublicAsset(assetKey) + + assertEquals(PublicAssetResult.Failure::class, publicAsset::class) + assertEquals(CoreFailure.Unknown::class, (publicAsset as PublicAssetResult.Failure).coreFailure::class) + + verify(assetRepository) + .suspendFunction(assetRepository::downloadPublicAsset) + .with(eq(assetKey)) + .wasInvoked(exactly = once) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt index 29eb636d05c..69777801a56 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt @@ -3,6 +3,8 @@ package com.wire.kalium.logic.feature.user import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.asset.UploadedAssetId +import com.wire.kalium.logic.data.user.SelfUser +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.functional.Either import io.mockative.Mock @@ -44,11 +46,12 @@ class UploadUserAvatarUseCaseTest { given(userRepository) .suspendFunction(userRepository::updateSelfUser) .whenInvokedWith(eq(null), eq(null), eq(expected.key)) - .thenReturn(Either.Right(Unit)) + .thenReturn(Either.Right(stubSelfUser())) val actual = uploadUserAvatarUseCase("image/jpg", "A".encodeToByteArray()) - assertEquals(UploadAvatarResult.Success, actual) + assertEquals(UploadAvatarResult.Success::class, actual::class) + assertEquals("some_key", (actual as UploadAvatarResult.Success).userAssetId) verify(assetRepository) .suspendFunction(assetRepository::uploadAndPersistPublicAsset) @@ -71,7 +74,8 @@ class UploadUserAvatarUseCaseTest { val actual = uploadUserAvatarUseCase("image/jpg", "A".encodeToByteArray()) - assertEquals(UploadAvatarResult.Failure, actual) + assertEquals(UploadAvatarResult.Failure::class, actual::class) + assertEquals(CoreFailure.Unknown::class, (actual as UploadAvatarResult.Failure).coreFailure::class) verify(assetRepository) .suspendFunction(assetRepository::uploadAndPersistPublicAsset) @@ -83,4 +87,16 @@ class UploadUserAvatarUseCaseTest { .with(eq(null), eq(null), eq(expected.key)) .wasNotInvoked() } + + private fun stubSelfUser() = SelfUser( + UserId("some_id", "some_domain"), + "some_name", + "some_handle", + "some_email", + null, + 1, + null, + "some_key", + "some_key" + ) } 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 077f453b40c..8bdc0237e47 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 @@ -19,10 +19,10 @@ data class UserEntity( val phone: String?, val accentId: Int, val team: String?, - val previewAssetId: UserAssetId, - val completeAssetId: UserAssetId + val previewAssetId: UserAssetId?, + val completeAssetId: UserAssetId? ) -internal typealias UserAssetId = String? +internal typealias UserAssetId = String interface UserDAO { suspend fun insertUser(user: UserEntity) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAO.kt index f08e17f7330..e2545e59ef6 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAO.kt @@ -1,5 +1,7 @@ package com.wire.kalium.persistence.dao.asset +import kotlinx.coroutines.flow.Flow + data class AssetEntity( val key: String, val domain: String, @@ -35,4 +37,6 @@ data class AssetEntity( interface AssetDAO { suspend fun insertAsset(assetEntity: AssetEntity) suspend fun insertAssets(assetsEntity: List) + suspend fun getAssetByKey(assetKey: String): Flow + suspend fun updateAsset(assetEntity: AssetEntity) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAOImpl.kt index 2c7a8f1bd0a..159fc4cb228 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAOImpl.kt @@ -1,6 +1,10 @@ package com.wire.kalium.persistence.dao.asset +import com.squareup.sqldelight.runtime.coroutines.asFlow +import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull import com.wire.kalium.persistence.db.AssetsQueries +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import com.wire.kalium.persistence.db.Asset as SQLDelightAsset class AssetMapper { @@ -42,4 +46,19 @@ class AssetDAOImpl(private val queries: AssetsQueries) : AssetDAO { } } } + + override suspend fun getAssetByKey(assetKey: String): Flow { + return queries.selectByKey(assetKey) + .asFlow() + .mapToOneOrNull() + .map { + it?.let { + return@map mapper.toModel(it) + } + } + } + + override suspend fun updateAsset(assetEntity: AssetEntity) { + queries.updateAsset(assetEntity.downloadedDate, assetEntity.rawData, assetEntity.mimeType, assetEntity.key) + } } diff --git a/persistence/src/commonMain/sqldelight/com/wire/kalium/persistence/db/Assets.sq b/persistence/src/commonMain/sqldelight/com/wire/kalium/persistence/db/Assets.sq index 6266daa2b81..dce5a1b3f7a 100644 --- a/persistence/src/commonMain/sqldelight/com/wire/kalium/persistence/db/Assets.sq +++ b/persistence/src/commonMain/sqldelight/com/wire/kalium/persistence/db/Assets.sq @@ -1,5 +1,3 @@ -import kotlin.Boolean; - CREATE TABLE Asset ( key TEXT NOT NULL, domain TEXT NOT NULL, diff --git a/persistence/src/commonMain/sqldelight/com/wire/kalium/persistence/db/Users.sq b/persistence/src/commonMain/sqldelight/com/wire/kalium/persistence/db/Users.sq index f713884ce68..c88d3898ef7 100644 --- a/persistence/src/commonMain/sqldelight/com/wire/kalium/persistence/db/Users.sq +++ b/persistence/src/commonMain/sqldelight/com/wire/kalium/persistence/db/Users.sq @@ -10,9 +10,7 @@ CREATE TABLE User ( accent_id INTEGER AS Int NOT NULL DEFAULT 0, team TEXT, preview_asset_id TEXT, - complete_asset_id TEXT, - FOREIGN KEY (preview_asset_id) REFERENCES Asset(key) ON DELETE SET NULL, - FOREIGN KEY (complete_asset_id) REFERENCES Asset(key) ON DELETE SET NULL + complete_asset_id TEXT ); deleteAllUsers: