mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 16:45:43 -05:00
Refactor Send logic into SendManager (#5892)
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
|
||||
/**
|
||||
* Manages the creating, updating, and deleting sends.
|
||||
*/
|
||||
interface SendManager {
|
||||
/**
|
||||
* Attempt to create a send. The [fileUri] _must_ be present when the given [SendView] has a
|
||||
* [SendView.type] of [SendType.FILE].
|
||||
*/
|
||||
suspend fun createSend(sendView: SendView, fileUri: Uri?): CreateSendResult
|
||||
|
||||
/**
|
||||
* Attempt to delete a send.
|
||||
*/
|
||||
suspend fun deleteSend(sendId: String): DeleteSendResult
|
||||
|
||||
/**
|
||||
* Attempt to remove the password from a send.
|
||||
*/
|
||||
suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult
|
||||
|
||||
/**
|
||||
* Attempt to update a send.
|
||||
*/
|
||||
suspend fun updateSend(
|
||||
sendId: String,
|
||||
sendView: SendView,
|
||||
): UpdateSendResult
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.model.CreateFileSendResponse
|
||||
import com.bitwarden.network.model.CreateSendJsonResponse
|
||||
import com.bitwarden.network.model.UpdateSendResponseJson
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.send.Send
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import retrofit2.HttpException
|
||||
|
||||
/**
|
||||
* The default implementation of the [SendManager].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class SendManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val sendsService: SendsService,
|
||||
private val fileManager: FileManager,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : SendManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
init {
|
||||
pushManager
|
||||
.syncSendDeleteFlow
|
||||
.onEach(::deleteSend)
|
||||
.launchIn(unconfinedScope)
|
||||
pushManager
|
||||
.syncSendUpsertFlow
|
||||
.onEach(::syncSendIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
}
|
||||
|
||||
override suspend fun createSend(
|
||||
sendView: SendView,
|
||||
fileUri: Uri?,
|
||||
): CreateSendResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateSendResult.Error(message = null, error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.encryptSend(userId = userId, sendView = sendView)
|
||||
.flatMap { send ->
|
||||
when (send.type) {
|
||||
SendType.TEXT -> sendsService.createTextSend(send.toEncryptedNetworkSend())
|
||||
SendType.FILE -> createFileSend(uri = fileUri, userId = userId, send = send)
|
||||
}
|
||||
}
|
||||
.map { createSendResponse ->
|
||||
when (createSendResponse) {
|
||||
is CreateSendJsonResponse.Invalid -> {
|
||||
return CreateSendResult.Error(
|
||||
message = createSendResponse.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
is CreateSendJsonResponse.Success -> {
|
||||
// Save the send immediately, regardless of whether the decrypt succeeds
|
||||
vaultDiskSource.saveSend(userId = userId, send = createSendResponse.send)
|
||||
createSendResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatMap { createSendSuccessResponse ->
|
||||
vaultSdkSource.decryptSend(
|
||||
userId = userId,
|
||||
send = createSendSuccessResponse.send.toEncryptedSdkSend(),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateSendResult.Error(message = null, error = it) },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerCreateSendAction()
|
||||
CreateSendResult.Success(sendView = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteSend(sendId: String): DeleteSendResult {
|
||||
val userId = activeUserId ?: return DeleteSendResult.Error(error = NoActiveUserException())
|
||||
return sendsService
|
||||
.deleteSend(sendId)
|
||||
.onSuccess { vaultDiskSource.deleteSend(userId = userId, sendId = sendId) }
|
||||
.fold(
|
||||
onSuccess = { DeleteSendResult.Success },
|
||||
onFailure = { DeleteSendResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult {
|
||||
val userId = activeUserId ?: return RemovePasswordSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = NoActiveUserException(),
|
||||
)
|
||||
return sendsService
|
||||
.removeSendPassword(sendId = sendId)
|
||||
.fold(
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is UpdateSendResponseJson.Invalid -> {
|
||||
RemovePasswordSendResult.Error(
|
||||
errorMessage = response.message,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
is UpdateSendResponseJson.Success -> {
|
||||
vaultDiskSource.saveSend(userId = userId, send = response.send)
|
||||
vaultSdkSource
|
||||
.decryptSend(
|
||||
userId = userId,
|
||||
send = response.send.toEncryptedSdkSend(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { RemovePasswordSendResult.Success(sendView = it) },
|
||||
onFailure = {
|
||||
RemovePasswordSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { RemovePasswordSendResult.Error(errorMessage = null, error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateSend(
|
||||
sendId: String,
|
||||
sendView: SendView,
|
||||
): UpdateSendResult {
|
||||
val userId = activeUserId ?: return UpdateSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = NoActiveUserException(),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.encryptSend(userId = userId, sendView = sendView)
|
||||
.flatMap { send ->
|
||||
sendsService.updateSend(sendId = sendId, body = send.toEncryptedNetworkSend())
|
||||
}
|
||||
.fold(
|
||||
onFailure = { UpdateSendResult.Error(errorMessage = null, error = it) },
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is UpdateSendResponseJson.Invalid -> {
|
||||
UpdateSendResult.Error(errorMessage = response.message, error = null)
|
||||
}
|
||||
|
||||
is UpdateSendResponseJson.Success -> {
|
||||
vaultDiskSource.saveSend(userId = userId, send = response.send)
|
||||
vaultSdkSource
|
||||
.decryptSend(
|
||||
userId = userId,
|
||||
send = response.send.toEncryptedSdkSend(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { UpdateSendResult.Success(sendView = it) },
|
||||
onFailure = {
|
||||
UpdateSendResult.Error(errorMessage = null, error = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createFileSend(
|
||||
uri: Uri?,
|
||||
userId: String,
|
||||
send: Send,
|
||||
): Result<CreateSendJsonResponse> {
|
||||
uri ?: return IllegalArgumentException("File URI must be present to create a File Send.")
|
||||
.asFailure()
|
||||
|
||||
return fileManager
|
||||
.writeUriToCache(uri)
|
||||
.flatMap { file ->
|
||||
vaultSdkSource.encryptFile(
|
||||
userId = userId,
|
||||
send = send,
|
||||
path = file.absolutePath,
|
||||
destinationFilePath = file.absolutePath,
|
||||
)
|
||||
}
|
||||
.flatMap { encryptedFile ->
|
||||
sendsService
|
||||
.createFileSend(
|
||||
body = send.toEncryptedNetworkSend(fileLength = encryptedFile.length()),
|
||||
)
|
||||
.flatMap { sendFileResponse ->
|
||||
when (sendFileResponse) {
|
||||
is CreateFileSendResponse.Invalid -> {
|
||||
CreateSendJsonResponse
|
||||
.Invalid(
|
||||
message = sendFileResponse.message,
|
||||
validationErrors = sendFileResponse.validationErrors,
|
||||
)
|
||||
.asSuccess()
|
||||
}
|
||||
|
||||
is CreateFileSendResponse.Success -> {
|
||||
sendsService
|
||||
.uploadFile(
|
||||
sendFileResponse = sendFileResponse.createFileJsonResponse,
|
||||
encryptedFile = encryptedFile,
|
||||
)
|
||||
.also {
|
||||
// Delete encrypted file once it has been uploaded.
|
||||
fileManager.delete(encryptedFile)
|
||||
}
|
||||
.map { CreateSendJsonResponse.Success(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the send specified by [syncSendDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteSend(syncSendDeleteData: SyncSendDeleteData) {
|
||||
vaultDiskSource.deleteSend(
|
||||
userId = syncSendDeleteData.userId,
|
||||
sendId = syncSendDeleteData.sendId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs an individual send contained in [syncSendUpsertData] to disk if certain criteria are
|
||||
* met. If the resource cannot be found cloud-side, and it was updated, delete it from disk for
|
||||
* now.
|
||||
*/
|
||||
private suspend fun syncSendIfNecessary(syncSendUpsertData: SyncSendUpsertData) {
|
||||
val userId = activeUserId ?: return
|
||||
val sendId = syncSendUpsertData.sendId
|
||||
val isUpdate = syncSendUpsertData.isUpdate
|
||||
val revisionDate = syncSendUpsertData.revisionDate
|
||||
val localSend = vaultDiskSource
|
||||
.getSends(userId = userId)
|
||||
.first()
|
||||
.find { it.id == sendId }
|
||||
val isValidCreate = !isUpdate && localSend == null
|
||||
val isValidUpdate = isUpdate &&
|
||||
localSend != null &&
|
||||
localSend.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
|
||||
if (!isValidCreate && !isValidUpdate) return
|
||||
|
||||
sendsService
|
||||
.getSend(sendId = sendId)
|
||||
.fold(
|
||||
onSuccess = { vaultDiskSource.saveSend(userId = userId, send = it) },
|
||||
onFailure = {
|
||||
// Delete any updates if it's missing from the server
|
||||
val httpException = it as? HttpException
|
||||
@Suppress("MagicNumber")
|
||||
if (httpException?.code() == 404 && isUpdate) {
|
||||
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,14 @@ import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
@@ -22,6 +24,8 @@ import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
@@ -63,6 +67,28 @@ object VaultManagerModule {
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendManager(
|
||||
sendsService: SendsService,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
authDiskSource: AuthDiskSource,
|
||||
fileManager: FileManager,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): SendManager = SendManagerImpl(
|
||||
fileManager = fileManager,
|
||||
authDiskSource = authDiskSource,
|
||||
sendsService = sendsService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
pushManager = pushManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFileManager(
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.collections.CollectionView
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherType
|
||||
@@ -15,22 +13,19 @@ import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
@@ -42,7 +37,7 @@ import javax.crypto.Cipher
|
||||
* Responsible for managing vault data inside the network layer.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface VaultRepository : CipherManager, VaultLockManager {
|
||||
interface VaultRepository : CipherManager, SendManager, VaultLockManager {
|
||||
|
||||
/**
|
||||
* The [VaultFilterType] for the current user.
|
||||
@@ -204,35 +199,11 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
pin: String,
|
||||
): VaultUnlockResult
|
||||
|
||||
/**
|
||||
* Attempt to create a send. The [fileUri] _must_ be present when the given [SendView] has a
|
||||
* [SendView.type] of [SendType.FILE].
|
||||
*/
|
||||
suspend fun createSend(sendView: SendView, fileUri: Uri?): CreateSendResult
|
||||
|
||||
/**
|
||||
* Attempt to update a send.
|
||||
*/
|
||||
suspend fun updateSend(
|
||||
sendId: String,
|
||||
sendView: SendView,
|
||||
): UpdateSendResult
|
||||
|
||||
/**
|
||||
* Attempt to remove the password from a send.
|
||||
*/
|
||||
suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult
|
||||
|
||||
/**
|
||||
* Attempt to get the verification code and the period.
|
||||
*/
|
||||
suspend fun generateTotp(cipherId: String, time: DateTime): GenerateTotpResult
|
||||
|
||||
/**
|
||||
* Attempt to delete a send.
|
||||
*/
|
||||
suspend fun deleteSend(sendId: String): DeleteSendResult
|
||||
|
||||
/**
|
||||
* Attempt to create a folder.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.collections.CollectionView
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
@@ -11,22 +10,15 @@ import com.bitwarden.core.data.repository.util.map
|
||||
import com.bitwarden.core.data.repository.util.mapNullable
|
||||
import com.bitwarden.core.data.repository.util.updateToPendingOrLoading
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.exporters.ExportFormat
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.network.model.CreateFileSendResponse
|
||||
import com.bitwarden.network.model.CreateSendJsonResponse
|
||||
import com.bitwarden.network.model.UpdateFolderResponseJson
|
||||
import com.bitwarden.network.model.UpdateSendResponseJson
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.util.isNoConnectionError
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.send.Send
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherListViewType
|
||||
@@ -43,20 +35,17 @@ import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderUpsertData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndUnlocked
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
|
||||
@@ -65,31 +54,25 @@ import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SendData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabetically
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabeticallyByTypeAndOrganization
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkFolder
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toSdkAccount
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
@@ -138,25 +121,24 @@ private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L
|
||||
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
|
||||
class VaultRepositoryImpl(
|
||||
private val ciphersService: CiphersService,
|
||||
private val sendsService: SendsService,
|
||||
private val folderService: FolderService,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
private val cipherManager: CipherManager,
|
||||
private val fileManager: FileManager,
|
||||
private val sendManager: SendManager,
|
||||
private val vaultLockManager: VaultLockManager,
|
||||
private val totpCodeManager: TotpCodeManager,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
pushManager: PushManager,
|
||||
private val clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
private val vaultSyncManager: VaultSyncManager,
|
||||
private val credentialExchangeImportManager: CredentialExchangeImportManager,
|
||||
) : VaultRepository,
|
||||
CipherManager by cipherManager,
|
||||
SendManager by sendManager,
|
||||
VaultLockManager by vaultLockManager {
|
||||
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
@@ -314,16 +296,6 @@ class VaultRepositoryImpl(
|
||||
.onEach(::syncCipherIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
|
||||
pushManager
|
||||
.syncSendDeleteFlow
|
||||
.onEach(::deleteSend)
|
||||
.launchIn(unconfinedScope)
|
||||
|
||||
pushManager
|
||||
.syncSendUpsertFlow
|
||||
.onEach(::syncSendIfNecessary)
|
||||
.launchIn(ioScope)
|
||||
|
||||
pushManager
|
||||
.syncFolderDeleteFlow
|
||||
.onEach(::deleteFolder)
|
||||
@@ -657,153 +629,6 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createSend(
|
||||
sendView: SendView,
|
||||
fileUri: Uri?,
|
||||
): CreateSendResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateSendResult.Error(message = null, error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.encryptSend(
|
||||
userId = userId,
|
||||
sendView = sendView,
|
||||
)
|
||||
.flatMap { send ->
|
||||
when (send.type) {
|
||||
SendType.TEXT -> sendsService.createTextSend(send.toEncryptedNetworkSend())
|
||||
SendType.FILE -> createFileSend(fileUri, userId, send)
|
||||
}
|
||||
}
|
||||
.map { createSendResponse ->
|
||||
when (createSendResponse) {
|
||||
is CreateSendJsonResponse.Invalid -> {
|
||||
return CreateSendResult.Error(
|
||||
message = createSendResponse.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
is CreateSendJsonResponse.Success -> {
|
||||
// Save the send immediately, regardless of whether the decrypt succeeds
|
||||
vaultDiskSource.saveSend(userId = userId, send = createSendResponse.send)
|
||||
createSendResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatMap { createSendSuccessResponse ->
|
||||
vaultSdkSource.decryptSend(
|
||||
userId = userId,
|
||||
send = createSendSuccessResponse.send.toEncryptedSdkSend(),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateSendResult.Error(message = null, error = it) },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerCreateSendAction()
|
||||
CreateSendResult.Success(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateSend(
|
||||
sendId: String,
|
||||
sendView: SendView,
|
||||
): UpdateSendResult {
|
||||
val userId = activeUserId
|
||||
?: return UpdateSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = NoActiveUserException(),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.encryptSend(
|
||||
userId = userId,
|
||||
sendView = sendView,
|
||||
)
|
||||
.flatMap { send ->
|
||||
sendsService.updateSend(
|
||||
sendId = sendId,
|
||||
body = send.toEncryptedNetworkSend(),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { UpdateSendResult.Error(errorMessage = null, error = it) },
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is UpdateSendResponseJson.Invalid -> {
|
||||
UpdateSendResult.Error(errorMessage = response.message, error = null)
|
||||
}
|
||||
|
||||
is UpdateSendResponseJson.Success -> {
|
||||
vaultDiskSource.saveSend(userId = userId, send = response.send)
|
||||
vaultSdkSource
|
||||
.decryptSend(
|
||||
userId = userId,
|
||||
send = response.send.toEncryptedSdkSend(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { UpdateSendResult.Success(sendView = it) },
|
||||
onFailure = {
|
||||
UpdateSendResult.Error(errorMessage = null, error = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult {
|
||||
val userId = activeUserId
|
||||
?: return RemovePasswordSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = NoActiveUserException(),
|
||||
)
|
||||
return sendsService
|
||||
.removeSendPassword(sendId = sendId)
|
||||
.fold(
|
||||
onSuccess = { response ->
|
||||
when (response) {
|
||||
is UpdateSendResponseJson.Invalid -> {
|
||||
RemovePasswordSendResult.Error(
|
||||
errorMessage = response.message,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
is UpdateSendResponseJson.Success -> {
|
||||
vaultDiskSource.saveSend(userId = userId, send = response.send)
|
||||
vaultSdkSource
|
||||
.decryptSend(
|
||||
userId = userId,
|
||||
send = response.send.toEncryptedSdkSend(),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { RemovePasswordSendResult.Success(sendView = it) },
|
||||
onFailure = {
|
||||
RemovePasswordSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { RemovePasswordSendResult.Error(errorMessage = null, error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteSend(sendId: String): DeleteSendResult {
|
||||
val userId = activeUserId ?: return DeleteSendResult.Error(error = NoActiveUserException())
|
||||
return sendsService
|
||||
.deleteSend(sendId)
|
||||
.onSuccess { vaultDiskSource.deleteSend(userId, sendId) }
|
||||
.fold(
|
||||
onSuccess = { DeleteSendResult.Success },
|
||||
onFailure = { DeleteSendResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun generateTotp(
|
||||
cipherId: String,
|
||||
time: DateTime,
|
||||
@@ -1317,108 +1142,6 @@ class VaultRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createFileSend(
|
||||
uri: Uri?,
|
||||
userId: String,
|
||||
send: Send,
|
||||
): Result<CreateSendJsonResponse> {
|
||||
uri ?: return IllegalArgumentException(
|
||||
"File URI must be present to create a File Send.",
|
||||
)
|
||||
.asFailure()
|
||||
|
||||
return fileManager
|
||||
.writeUriToCache(uri)
|
||||
.flatMap { file ->
|
||||
vaultSdkSource.encryptFile(
|
||||
userId = userId,
|
||||
send = send,
|
||||
path = file.absolutePath,
|
||||
destinationFilePath = file.absolutePath,
|
||||
)
|
||||
}
|
||||
.flatMap { encryptedFile ->
|
||||
sendsService
|
||||
.createFileSend(
|
||||
body = send.toEncryptedNetworkSend(
|
||||
fileLength = encryptedFile.length(),
|
||||
),
|
||||
)
|
||||
.flatMap { sendFileResponse ->
|
||||
when (sendFileResponse) {
|
||||
is CreateFileSendResponse.Invalid -> {
|
||||
CreateSendJsonResponse
|
||||
.Invalid(
|
||||
message = sendFileResponse.message,
|
||||
validationErrors = sendFileResponse.validationErrors,
|
||||
)
|
||||
.asSuccess()
|
||||
}
|
||||
|
||||
is CreateFileSendResponse.Success -> {
|
||||
sendsService
|
||||
.uploadFile(
|
||||
sendFileResponse = sendFileResponse.createFileJsonResponse,
|
||||
encryptedFile = encryptedFile,
|
||||
)
|
||||
.also {
|
||||
// Delete encrypted file once it has been uploaded.
|
||||
fileManager.delete(encryptedFile)
|
||||
}
|
||||
.map { CreateSendJsonResponse.Success(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the send specified by [syncSendDeleteData] from disk.
|
||||
*/
|
||||
private suspend fun deleteSend(syncSendDeleteData: SyncSendDeleteData) {
|
||||
vaultDiskSource.deleteSend(
|
||||
userId = syncSendDeleteData.userId,
|
||||
sendId = syncSendDeleteData.sendId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs an individual send contained in [syncSendUpsertData] to disk if certain criteria are
|
||||
* met. If the resource cannot be found cloud-side, and it was updated, delete it from disk for
|
||||
* now.
|
||||
*/
|
||||
private suspend fun syncSendIfNecessary(syncSendUpsertData: SyncSendUpsertData) {
|
||||
val userId = activeUserId ?: return
|
||||
val sendId = syncSendUpsertData.sendId
|
||||
val isUpdate = syncSendUpsertData.isUpdate
|
||||
val revisionDate = syncSendUpsertData.revisionDate
|
||||
|
||||
val localSend = vaultDiskSource
|
||||
.getSends(userId = userId)
|
||||
.first()
|
||||
.find { it.id == sendId }
|
||||
val isValidCreate = !isUpdate && localSend == null
|
||||
val isValidUpdate = isUpdate &&
|
||||
localSend != null &&
|
||||
localSend.revisionDate.toEpochSecond() < revisionDate.toEpochSecond()
|
||||
|
||||
if (!isValidCreate && !isValidUpdate) return
|
||||
|
||||
sendsService
|
||||
.getSend(sendId)
|
||||
.fold(
|
||||
onSuccess = { vaultDiskSource.saveSend(userId, it) },
|
||||
onFailure = {
|
||||
// Delete any updates if it's missing from the server
|
||||
val httpException = it as? HttpException
|
||||
@Suppress("MagicNumber")
|
||||
if (httpException?.code() == 404 && isUpdate) {
|
||||
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the folder specified by [syncFolderDeleteData] from disk.
|
||||
*/
|
||||
|
||||
@@ -3,17 +3,15 @@ package com.x8bit.bitwarden.data.vault.repository.di
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.CipherManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.SendManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager
|
||||
@@ -36,7 +34,6 @@ object VaultRepositoryModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesVaultRepository(
|
||||
sendsService: SendsService,
|
||||
ciphersService: CiphersService,
|
||||
folderService: FolderService,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
@@ -44,18 +41,16 @@ object VaultRepositoryModule {
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
cipherManager: CipherManager,
|
||||
fileManager: FileManager,
|
||||
sendManager: SendManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
totpCodeManager: TotpCodeManager,
|
||||
pushManager: PushManager,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
clock: Clock,
|
||||
reviewPromptManager: ReviewPromptManager,
|
||||
vaultSyncManager: VaultSyncManager,
|
||||
credentialExchangeImportManager: CredentialExchangeImportManager,
|
||||
): VaultRepository = VaultRepositoryImpl(
|
||||
sendsService = sendsService,
|
||||
ciphersService = ciphersService,
|
||||
folderService = folderService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
@@ -63,14 +58,13 @@ object VaultRepositoryModule {
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
cipherManager = cipherManager,
|
||||
fileManager = fileManager,
|
||||
sendManager = sendManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
totpCodeManager = totpCodeManager,
|
||||
pushManager = pushManager,
|
||||
databaseSchemeManager = databaseSchemeManager,
|
||||
clock = clock,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
vaultSyncManager = vaultSyncManager,
|
||||
credentialExchangeImportManager = credentialExchangeImportManager,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,947 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.network.model.CreateFileSendResponse
|
||||
import com.bitwarden.network.model.CreateSendJsonResponse
|
||||
import com.bitwarden.network.model.SendTypeJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.UpdateSendResponseJson
|
||||
import com.bitwarden.network.model.createMockFileSendResponseJson
|
||||
import com.bitwarden.network.model.createMockSend
|
||||
import com.bitwarden.network.model.createMockSendJsonRequest
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.send.SendType
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSend
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkConstructor
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class SendManagerTest {
|
||||
private val fileManager: FileManager = mockk {
|
||||
coEvery { delete(files = anyVararg()) } just runs
|
||||
}
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val sendsService = mockk<SendsService>()
|
||||
private val vaultDiskSource = mockk<VaultDiskSource>()
|
||||
private val vaultSdkSource = mockk<VaultSdkSource>()
|
||||
private val reviewPromptManager = mockk<ReviewPromptManager> {
|
||||
every { registerCreateSendAction() } just runs
|
||||
}
|
||||
private val mutableSyncSendDeleteFlow = bufferedMutableSharedFlow<SyncSendDeleteData>()
|
||||
private val mutableSyncSendUpsertFlow = bufferedMutableSharedFlow<SyncSendUpsertData>()
|
||||
private val pushManager: PushManager = mockk {
|
||||
every { syncSendDeleteFlow } returns mutableSyncSendDeleteFlow
|
||||
every { syncSendUpsertFlow } returns mutableSyncSendUpsertFlow
|
||||
}
|
||||
|
||||
private val sendManager: SendManager = SendManagerImpl(
|
||||
sendsService = sendsService,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
fileManager = fileManager,
|
||||
reviewPromptManager = reviewPromptManager,
|
||||
pushManager = pushManager,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(Uri::class)
|
||||
mockkConstructor(NoActiveUserException::class, MissingPropertyException::class)
|
||||
every {
|
||||
anyConstructed<NoActiveUserException>() == any<NoActiveUserException>()
|
||||
} returns true
|
||||
every {
|
||||
anyConstructed<MissingPropertyException>() == any<MissingPropertyException>()
|
||||
} returns true
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(Uri::class)
|
||||
unmockkConstructor(NoActiveUserException::class, MissingPropertyException::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncSendDeleteFlow should delete send from disk`() {
|
||||
val userId = "mockId-1"
|
||||
val sendId = "mockId-1"
|
||||
coEvery { vaultDiskSource.deleteSend(userId = userId, sendId = sendId) } just runs
|
||||
|
||||
mutableSyncSendDeleteFlow.tryEmit(SyncSendDeleteData(userId = userId, sendId = sendId))
|
||||
|
||||
coVerify { vaultDiskSource.deleteSend(userId = userId, sendId = sendId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncSendUpsertFlow create with local send should do nothing`() = runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-$number"
|
||||
val send = createMockSend(number = 1, id = sendId)
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultDiskSource.getSends(userId = userId) } returns MutableStateFlow(listOf(send))
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
|
||||
isUpdate = false,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
sendsService.getSend(sendId = sendId)
|
||||
vaultDiskSource.saveSend(userId = userId, send = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncSendUpsertFlow update with no local send should do nothing`() = runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery { vaultDiskSource.getSends(userId = userId) } returns MutableStateFlow(emptyList())
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
sendsService.getSend(sendId = sendId)
|
||||
vaultDiskSource.saveSend(userId = userId, send = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncSendUpsertFlow update with more recent local send should do nothing`() = runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val send = createMockSend(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
|
||||
)
|
||||
val updatedSend = createMockSend(number = number)
|
||||
coEvery { vaultDiskSource.getSends(userId = userId) } returns MutableStateFlow(listOf(send))
|
||||
coEvery { sendsService.getSend(sendId = sendId) } returns updatedSend.asSuccess()
|
||||
coEvery { vaultDiskSource.saveSend(userId = userId, send = updatedSend) } just runs
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK).minus(5, ChronoUnit.MINUTES),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
sendsService.getSend(sendId = sendId)
|
||||
vaultDiskSource.saveSend(userId = userId, send = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncSendUpsertFlow update failure with 404 code should make a request for a send and then delete it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val response: HttpException = mockk {
|
||||
every { code() } returns 404
|
||||
}
|
||||
coEvery { sendsService.getSend(sendId = sendId) } returns response.asFailure()
|
||||
coEvery {
|
||||
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
|
||||
} just runs
|
||||
|
||||
val sendView = createMockSend(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
coEvery {
|
||||
vaultDiskSource.getSends(userId = userId)
|
||||
} returns MutableStateFlow(listOf(sendView))
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
sendsService.getSend(sendId = sendId)
|
||||
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncSendUpsertFlow create failure with 404 code should make a request for a send and do nothing`() =
|
||||
runTest {
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-1"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val response: HttpException = mockk {
|
||||
every { code() } returns 404
|
||||
}
|
||||
coEvery { sendsService.getSend(sendId = sendId) } returns response.asFailure()
|
||||
coEvery {
|
||||
vaultDiskSource.getSends(userId = userId)
|
||||
} returns MutableStateFlow(emptyList())
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
|
||||
isUpdate = false,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
sendsService.getSend(sendId = sendId)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
vaultDiskSource.deleteSend(userId = userId, sendId = sendId)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncSendUpsertFlow valid create success should make a request for a send and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultDiskSource.getSends(userId = userId)
|
||||
} returns MutableStateFlow(emptyList())
|
||||
val send = mockk<SyncResponseJson.Send>()
|
||||
coEvery { sendsService.getSend(sendId = sendId) } returns send.asSuccess()
|
||||
coEvery { vaultDiskSource.saveSend(userId = userId, send = send) } just runs
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
|
||||
isUpdate = false,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
sendsService.getSend(sendId = sendId)
|
||||
vaultDiskSource.saveSend(userId = userId, send = send)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncSendUpsertFlow valid update success should make a request for a send and then store it`() =
|
||||
runTest {
|
||||
val number = 1
|
||||
val userId = MOCK_USER_STATE.activeUserId
|
||||
val sendId = "mockId-$number"
|
||||
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val sendView = createMockSend(
|
||||
number = number,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK).minus(5, ChronoUnit.MINUTES),
|
||||
)
|
||||
coEvery {
|
||||
vaultDiskSource.getSends(userId = userId)
|
||||
} returns MutableStateFlow(listOf(sendView))
|
||||
|
||||
val send = mockk<SyncResponseJson.Send>()
|
||||
coEvery { sendsService.getSend(sendId = sendId) } returns send.asSuccess()
|
||||
coEvery { vaultDiskSource.saveSend(userId = userId, send = send) } just runs
|
||||
|
||||
mutableSyncSendUpsertFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
sendId = sendId,
|
||||
revisionDate = ZonedDateTime.now(FIXED_CLOCK),
|
||||
isUpdate = true,
|
||||
),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
sendsService.getSend(sendId = sendId)
|
||||
vaultDiskSource.saveSend(userId = userId, send = send)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createSend with no active user should return CreateSendResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = sendManager.createSend(
|
||||
sendView = mockk(),
|
||||
fileUri = mockk(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
CreateSendResult.Error(message = null, error = NoActiveUserException()),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createSend with encryptSend failure should return CreateSendResult failure`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val mockSendView = createMockSendView(number = 1)
|
||||
val error = IllegalStateException()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = sendManager.createSend(sendView = mockSendView, fileUri = null)
|
||||
|
||||
assertEquals(CreateSendResult.Error(message = null, error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `createSend with TEXT and sendsService createTextSend failure should return CreateSendResult failure`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val mockSendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
val error = IllegalStateException()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess()
|
||||
coEvery {
|
||||
sendsService.createTextSend(
|
||||
body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT)
|
||||
.copy(fileLength = null),
|
||||
)
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = sendManager.createSend(sendView = mockSendView, fileUri = null)
|
||||
|
||||
assertEquals(CreateSendResult.Error(message = error.message, error = error), result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createSend with TEXT and sendsService createTextSend success should return CreateSendResult success and increment send action count`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val mockSendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
val mockSdkSend = createMockSdkSend(number = 1, type = SendType.TEXT)
|
||||
val mockSend = createMockSend(number = 1, type = SendTypeJson.TEXT)
|
||||
val mockSendViewResult = createMockSendView(number = 2)
|
||||
val sendTextResponse = CreateSendJsonResponse.Success(send = mockSend)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns mockSdkSend.asSuccess()
|
||||
coEvery {
|
||||
sendsService.createTextSend(
|
||||
body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT)
|
||||
.copy(fileLength = null),
|
||||
)
|
||||
} returns sendTextResponse.asSuccess()
|
||||
coEvery { vaultDiskSource.saveSend(userId, mockSend) } just runs
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSend(userId, mockSdkSend)
|
||||
} returns mockSendViewResult.asSuccess()
|
||||
|
||||
val result = sendManager.createSend(sendView = mockSendView, fileUri = null)
|
||||
|
||||
assertEquals(CreateSendResult.Success(mockSendViewResult), result)
|
||||
|
||||
verify(exactly = 1) { reviewPromptManager.registerCreateSendAction() }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `createSend with FILE and sendsService createFileSend failure should return CreateSendResult failure`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val uri = setupMockUri(url = "www.test.com")
|
||||
val mockSendView = createMockSendView(number = 1)
|
||||
val mockSdkSend = createMockSdkSend(number = 1)
|
||||
val decryptedFile = mockk<File> {
|
||||
every { length() } returns 1
|
||||
every { absolutePath } returns "mockAbsolutePath"
|
||||
}
|
||||
val encryptedFile = mockk<File> {
|
||||
every { length() } returns 1
|
||||
every { absolutePath } returns "mockAbsolutePath"
|
||||
}
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns mockSdkSend.asSuccess()
|
||||
coEvery { fileManager.writeUriToCache(any()) } returns decryptedFile.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptFile(
|
||||
userId = userId,
|
||||
send = mockSdkSend,
|
||||
path = "mockAbsolutePath",
|
||||
destinationFilePath = "mockAbsolutePath",
|
||||
)
|
||||
} returns encryptedFile.asSuccess()
|
||||
val error = IllegalStateException()
|
||||
coEvery {
|
||||
sendsService.createFileSend(body = createMockSendJsonRequest(number = 1))
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = sendManager.createSend(sendView = mockSendView, fileUri = uri)
|
||||
|
||||
assertEquals(CreateSendResult.Error(message = error.message, error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `createSend with FILE and sendsService uploadFile failure should return CreateSendResult failure`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val url = "www.test.com"
|
||||
val uri = setupMockUri(url = url)
|
||||
val mockSendView = createMockSendView(number = 1)
|
||||
val mockSdkSend = createMockSdkSend(number = 1)
|
||||
val decryptedFile = mockk<File> {
|
||||
every { name } returns "mockFileName"
|
||||
every { absolutePath } returns "mockAbsolutePath"
|
||||
every { length() } returns 1
|
||||
}
|
||||
val encryptedFile = mockk<File> {
|
||||
every { name } returns "mockFileName"
|
||||
every { absolutePath } returns "mockAbsolutePath"
|
||||
every { length() } returns 1
|
||||
}
|
||||
val sendFileResponse = CreateFileSendResponse.Success(
|
||||
createMockFileSendResponseJson(number = 1),
|
||||
)
|
||||
val error = Throwable()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns mockSdkSend.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSend(userId, mockSdkSend)
|
||||
} returns mockSendView.asSuccess()
|
||||
every { fileManager.filesDirectory } returns "mockFilesDirectory"
|
||||
coEvery { fileManager.writeUriToCache(any()) } returns decryptedFile.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptFile(
|
||||
userId = userId,
|
||||
send = mockSdkSend,
|
||||
path = "mockAbsolutePath",
|
||||
destinationFilePath = "mockAbsolutePath",
|
||||
)
|
||||
} returns encryptedFile.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveSend(
|
||||
userId,
|
||||
sendFileResponse.createFileJsonResponse.sendResponse,
|
||||
)
|
||||
} just runs
|
||||
coEvery {
|
||||
sendsService.createFileSend(body = createMockSendJsonRequest(number = 1))
|
||||
} returns sendFileResponse.asSuccess()
|
||||
coEvery {
|
||||
sendsService.uploadFile(
|
||||
sendFileResponse = sendFileResponse.createFileJsonResponse,
|
||||
encryptedFile = encryptedFile,
|
||||
)
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = sendManager.createSend(sendView = mockSendView, fileUri = uri)
|
||||
|
||||
assertEquals(CreateSendResult.Error(message = null, error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `createSend with FILE and fileManager uriToByteArray failure should return CreateSendResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val url = "www.test.com"
|
||||
val uri = setupMockUri(url = url)
|
||||
val mockSendView = createMockSendView(number = 1)
|
||||
val mockSdkSend = createMockSdkSend(number = 1)
|
||||
val error = Throwable()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns mockSdkSend.asSuccess()
|
||||
coEvery { fileManager.writeUriToCache(any()) } returns error.asFailure()
|
||||
|
||||
val result = sendManager.createSend(sendView = mockSendView, fileUri = uri)
|
||||
|
||||
assertEquals(CreateSendResult.Error(message = null, error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `createSend with FILE and sendsService uploadFile success should return CreateSendResult success`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val url = "www.test.com"
|
||||
val uri = setupMockUri(url = url)
|
||||
val mockSendView = createMockSendView(number = 1)
|
||||
val mockSdkSend = createMockSdkSend(number = 1)
|
||||
val decryptedFile = mockk<File> {
|
||||
every { name } returns "mockFileName"
|
||||
every { absolutePath } returns "mockAbsolutePath"
|
||||
}
|
||||
val encryptedFile = mockk<File> {
|
||||
every { length() } returns 1
|
||||
}
|
||||
val sendFileResponse = CreateFileSendResponse.Success(
|
||||
createMockFileSendResponseJson(number = 1),
|
||||
)
|
||||
val mockSendViewResult = createMockSendView(number = 1)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns mockSdkSend.asSuccess()
|
||||
|
||||
every { fileManager.filesDirectory } returns "mockFilesDirectory"
|
||||
coEvery { fileManager.writeUriToCache(any()) } returns decryptedFile.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptFile(
|
||||
userId = userId,
|
||||
send = mockSdkSend,
|
||||
path = "mockAbsolutePath",
|
||||
destinationFilePath = "mockAbsolutePath",
|
||||
)
|
||||
} returns encryptedFile.asSuccess()
|
||||
coEvery {
|
||||
sendsService.createFileSend(body = createMockSendJsonRequest(number = 1))
|
||||
} returns sendFileResponse.asSuccess()
|
||||
coEvery {
|
||||
sendsService.uploadFile(
|
||||
sendFileResponse = sendFileResponse.createFileJsonResponse,
|
||||
encryptedFile = encryptedFile,
|
||||
)
|
||||
} returns sendFileResponse.createFileJsonResponse.sendResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.saveSend(
|
||||
userId,
|
||||
sendFileResponse.createFileJsonResponse.sendResponse,
|
||||
)
|
||||
} just runs
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSend(userId, mockSdkSend)
|
||||
} returns mockSendViewResult.asSuccess()
|
||||
|
||||
val result = sendManager.createSend(sendView = mockSendView, fileUri = uri)
|
||||
|
||||
assertEquals(CreateSendResult.Success(mockSendViewResult), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteSend with no active user should return DeleteSendResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = sendManager.deleteSend(sendId = "sendId")
|
||||
|
||||
assertEquals(
|
||||
DeleteSendResult.Error(error = NoActiveUserException()),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteSend with sendsService deleteSend failure should return DeleteSendResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val sendId = "mockId-1"
|
||||
val error = Throwable("Fail")
|
||||
coEvery {
|
||||
sendsService.deleteSend(sendId = sendId)
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = sendManager.deleteSend(sendId)
|
||||
|
||||
assertEquals(DeleteSendResult.Error(error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteSend with sendsService deleteSend success should return DeleteSendResult success`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val sendId = "mockId-1"
|
||||
coEvery { sendsService.deleteSend(sendId = sendId) } returns Unit.asSuccess()
|
||||
coEvery { vaultDiskSource.deleteSend(userId, sendId) } just runs
|
||||
|
||||
val result = sendManager.deleteSend(sendId)
|
||||
|
||||
assertEquals(DeleteSendResult.Success, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removePasswordSend with no active user should return RemovePasswordSendResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = sendManager.removePasswordSend(sendId = "sendId")
|
||||
|
||||
assertEquals(
|
||||
RemovePasswordSendResult.Error(
|
||||
errorMessage = null,
|
||||
error = NoActiveUserException(),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `removePasswordSend with sendsService removeSendPassword Error should return RemovePasswordSendResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val sendId = "sendId1234"
|
||||
val error = Throwable("Fail")
|
||||
coEvery {
|
||||
sendsService.removeSendPassword(sendId = sendId)
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = sendManager.removePasswordSend(sendId = sendId)
|
||||
|
||||
assertEquals(RemovePasswordSendResult.Error(errorMessage = null, error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `removePasswordSend with sendsService removeSendPassword Success and vaultSdkSource decryptSend Failure should return RemovePasswordSendResult Error`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val sendId = "sendId1234"
|
||||
val mockSend = createMockSend(number = 1)
|
||||
val error = Throwable("Fail")
|
||||
coEvery {
|
||||
sendsService.removeSendPassword(sendId = sendId)
|
||||
} returns UpdateSendResponseJson.Success(send = mockSend).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSend(userId = userId, send = createMockSdkSend(number = 1))
|
||||
} returns error.asFailure()
|
||||
coEvery { vaultDiskSource.saveSend(userId = userId, send = mockSend) } just runs
|
||||
|
||||
val result = sendManager.removePasswordSend(sendId = sendId)
|
||||
|
||||
assertEquals(RemovePasswordSendResult.Error(errorMessage = null, error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `removePasswordSend with sendsService removeSendPassword Success should return RemovePasswordSendResult success`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val sendId = "sendId1234"
|
||||
val mockSendView = createMockSendView(number = 1)
|
||||
val mockSend = createMockSend(number = 1)
|
||||
coEvery {
|
||||
sendsService.removeSendPassword(sendId = sendId)
|
||||
} returns UpdateSendResponseJson.Success(send = mockSend).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSend(userId = userId, send = createMockSdkSend(number = 1))
|
||||
} returns mockSendView.asSuccess()
|
||||
coEvery { vaultDiskSource.saveSend(userId = userId, send = mockSend) } just runs
|
||||
|
||||
val result = sendManager.removePasswordSend(sendId = sendId)
|
||||
|
||||
assertEquals(RemovePasswordSendResult.Success(mockSendView), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateSend with no active user should return UpdateSendResult Error`() = runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = sendManager.updateSend(
|
||||
sendId = "sendId",
|
||||
sendView = mockk(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
UpdateSendResult.Error(errorMessage = null, error = NoActiveUserException()),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateSend with encryptSend failure should return UpdateSendResult failure`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val sendId = "sendId1234"
|
||||
val mockSendView = createMockSendView(number = 1)
|
||||
val error = IllegalStateException()
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = sendManager.updateSend(
|
||||
sendId = sendId,
|
||||
sendView = mockSendView,
|
||||
)
|
||||
|
||||
assertEquals(UpdateSendResult.Error(errorMessage = null, error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `updateSend with sendsService updateSend failure should return UpdateSendResult Error with a null message`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val sendId = "sendId1234"
|
||||
val mockSendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess()
|
||||
val error = IllegalStateException()
|
||||
coEvery {
|
||||
sendsService.updateSend(
|
||||
sendId = sendId,
|
||||
body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT)
|
||||
.copy(fileLength = null),
|
||||
)
|
||||
} returns error.asFailure()
|
||||
|
||||
val result = sendManager.updateSend(
|
||||
sendId = sendId,
|
||||
sendView = mockSendView,
|
||||
)
|
||||
|
||||
assertEquals(UpdateSendResult.Error(errorMessage = null, error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `updateSend with sendsService updateSend Invalid response should return UpdateSendResult Error with a non-null message`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val sendId = "sendId1234"
|
||||
val mockSendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess()
|
||||
coEvery {
|
||||
sendsService.updateSend(
|
||||
sendId = sendId,
|
||||
body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT)
|
||||
.copy(fileLength = null),
|
||||
)
|
||||
} returns UpdateSendResponseJson
|
||||
.Invalid(
|
||||
message = "You do not have permission to edit this.",
|
||||
validationErrors = null,
|
||||
)
|
||||
.asSuccess()
|
||||
|
||||
val result = sendManager.updateSend(
|
||||
sendId = sendId,
|
||||
sendView = mockSendView,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
UpdateSendResult.Error(
|
||||
errorMessage = "You do not have permission to edit this.",
|
||||
error = null,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `updateSend with sendsService updateSend success and decryption error should return UpdateSendResult Error with a null message`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val sendId = "sendId1234"
|
||||
val mockSendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess()
|
||||
val mockSend = createMockSend(number = 1, type = SendTypeJson.TEXT)
|
||||
coEvery {
|
||||
sendsService.updateSend(
|
||||
sendId = sendId,
|
||||
body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT)
|
||||
.copy(fileLength = null),
|
||||
)
|
||||
} returns UpdateSendResponseJson.Success(send = mockSend).asSuccess()
|
||||
val error = Throwable("Fail")
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSend(
|
||||
userId = userId, send = createMockSdkSend(number = 1, type = SendType.TEXT),
|
||||
)
|
||||
} returns error.asFailure()
|
||||
coEvery { vaultDiskSource.saveSend(userId = userId, send = mockSend) } just runs
|
||||
|
||||
val result = sendManager.updateSend(
|
||||
sendId = sendId,
|
||||
sendView = mockSendView,
|
||||
)
|
||||
|
||||
assertEquals(UpdateSendResult.Error(errorMessage = null, error = error), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `updateSend with sendsService updateSend Success response should return UpdateSendResult success`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
val sendId = "sendId1234"
|
||||
val mockSendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
coEvery {
|
||||
vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView)
|
||||
} returns createMockSdkSend(number = 1, type = SendType.TEXT).asSuccess()
|
||||
val mockSend = createMockSend(number = 1, type = SendTypeJson.TEXT)
|
||||
coEvery {
|
||||
sendsService.updateSend(
|
||||
sendId = sendId,
|
||||
body = createMockSendJsonRequest(number = 1, type = SendTypeJson.TEXT)
|
||||
.copy(fileLength = null),
|
||||
)
|
||||
} returns UpdateSendResponseJson.Success(send = mockSend).asSuccess()
|
||||
val mockSendViewResult = createMockSendView(number = 2, type = SendType.TEXT)
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSend(
|
||||
userId = userId,
|
||||
send = createMockSdkSend(number = 1, type = SendType.TEXT),
|
||||
)
|
||||
} returns mockSendViewResult.asSuccess()
|
||||
coEvery { vaultDiskSource.saveSend(userId = userId, send = mockSend) } just runs
|
||||
|
||||
val result = sendManager.updateSend(
|
||||
sendId = sendId,
|
||||
sendView = mockSendView,
|
||||
)
|
||||
|
||||
assertEquals(UpdateSendResult.Success(mockSendViewResult), result)
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
||||
private fun setupMockUri(
|
||||
url: String,
|
||||
queryParams: Map<String, String> = emptyMap(),
|
||||
): Uri {
|
||||
val mockUri = mockk<Uri> {
|
||||
queryParams.forEach {
|
||||
every { getQueryParameter(it.key) } returns it.value
|
||||
}
|
||||
}
|
||||
every { Uri.parse(url) } returns mockUri
|
||||
return mockUri
|
||||
}
|
||||
|
||||
//endregion Helper functions
|
||||
}
|
||||
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
private val MOCK_PROFILE = AccountJson.Profile(
|
||||
userId = "mockId-1",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = null,
|
||||
stamp = "mockSecurityStamp-1",
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremium = false,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = null,
|
||||
kdfIterations = null,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
|
||||
private val MOCK_ACCOUNT = AccountJson(
|
||||
profile = MOCK_PROFILE,
|
||||
tokens = AccountTokensJson(
|
||||
accessToken = "accessToken",
|
||||
refreshToken = "refreshToken",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = null,
|
||||
),
|
||||
)
|
||||
|
||||
private val MOCK_USER_STATE = UserStateJson(
|
||||
activeUserId = "mockId-1",
|
||||
accounts = mapOf(
|
||||
"mockId-1" to MOCK_ACCOUNT,
|
||||
),
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user