diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManager.kt new file mode 100644 index 0000000000..40c09d0748 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManager.kt @@ -0,0 +1,58 @@ +package com.x8bit.bitwarden.data.vault.manager + +import com.bitwarden.core.InitUserCryptoMethod +import com.bitwarden.core.Kdf +import com.x8bit.bitwarden.data.vault.repository.model.VaultState +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult +import kotlinx.coroutines.flow.StateFlow + +/** + * Manages the locking and unlocking of user vaults. + */ +interface VaultLockManager { + /** + * Flow that represents the current vault state. + */ + val vaultStateFlow: StateFlow + + /** + * Whether or not the vault is currently locked for the given [userId]. + */ + fun isVaultUnlocked(userId: String): Boolean + + /** + * Whether or not the vault is currently unlocking for the given [userId]. + */ + fun isVaultUnlocking(userId: String): Boolean + + /** + * Locks the vault for the user with the given [userId]. + */ + fun lockVault(userId: String) + + /** + * Locks the vault for the user with the given [userId] only if necessary. + */ + fun lockVaultIfNecessary(userId: String) + + /** + * Locks the vault for the current user if currently unlocked. + */ + fun lockVaultForCurrentUser() + + /** + * Attempt to unlock the vault with the specified user information. + * + * Note that when [organizationKeys] is absent, no attempt will be made to unlock the vault + * for organization data. + */ + @Suppress("LongParameterList") + suspend fun unlockVault( + userId: String, + email: String, + kdf: Kdf, + privateKey: String, + initUserCryptoMethod: InitUserCryptoMethod, + organizationKeys: Map?, + ): VaultUnlockResult +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt new file mode 100644 index 0000000000..dc306e80cc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt @@ -0,0 +1,149 @@ +package com.x8bit.bitwarden.data.vault.manager + +import com.bitwarden.core.InitOrgCryptoRequest +import com.bitwarden.core.InitUserCryptoMethod +import com.bitwarden.core.InitUserCryptoRequest +import com.bitwarden.core.Kdf +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource +import com.x8bit.bitwarden.data.platform.util.asSuccess +import com.x8bit.bitwarden.data.platform.util.flatMap +import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult +import com.x8bit.bitwarden.data.vault.repository.model.VaultState +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult +import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.update + +/** + * Primary implementation [VaultLockManager]. + */ +class VaultLockManagerImpl( + private val authDiskSource: AuthDiskSource, + private val vaultSdkSource: VaultSdkSource, +) : VaultLockManager { + private val activeUserId: String? get() = authDiskSource.userState?.activeUserId + + private val mutableVaultStateStateFlow = + MutableStateFlow( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + ) + + override val vaultStateFlow: StateFlow + get() = mutableVaultStateStateFlow.asStateFlow() + + override fun isVaultUnlocked(userId: String): Boolean = + userId in mutableVaultStateStateFlow.value.unlockedVaultUserIds + + override fun isVaultUnlocking(userId: String): Boolean = + userId in mutableVaultStateStateFlow.value.unlockingVaultUserIds + + override fun lockVault(userId: String) { + setVaultToLocked(userId = userId) + } + + override fun lockVaultForCurrentUser() { + activeUserId?.let { + lockVaultIfNecessary(it) + } + } + + override fun lockVaultIfNecessary(userId: String) { + // TODO: Check for VaultTimeout.Never (BIT-1019) + lockVault(userId = userId) + } + + override suspend fun unlockVault( + userId: String, + email: String, + kdf: Kdf, + privateKey: String, + initUserCryptoMethod: InitUserCryptoMethod, + organizationKeys: Map?, + ): VaultUnlockResult = + flow { + setVaultToUnlocking(userId = userId) + emit( + vaultSdkSource + .initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = initUserCryptoMethod, + ), + ) + .flatMap { result -> + // Initialize the SDK for organizations if necessary + if (organizationKeys != null && + result is InitializeCryptoResult.Success + ) { + vaultSdkSource.initializeOrganizationCrypto( + userId = userId, + request = InitOrgCryptoRequest( + organizationKeys = organizationKeys, + ), + ) + } else { + result.asSuccess() + } + } + .fold( + onFailure = { VaultUnlockResult.GenericError }, + onSuccess = { initializeCryptoResult -> + initializeCryptoResult + .toVaultUnlockResult() + .also { + if (it is VaultUnlockResult.Success) { + setVaultToUnlocked(userId = userId) + } + } + }, + ), + ) + } + .onCompletion { setVaultToNotUnlocking(userId = userId) } + .first() + + private fun setVaultToUnlocked(userId: String) { + mutableVaultStateStateFlow.update { + it.copy( + unlockedVaultUserIds = it.unlockedVaultUserIds + userId, + ) + } + } + + private fun setVaultToLocked(userId: String) { + vaultSdkSource.clearCrypto(userId = userId) + mutableVaultStateStateFlow.update { + it.copy( + unlockedVaultUserIds = it.unlockedVaultUserIds - userId, + ) + } + } + + private fun setVaultToUnlocking(userId: String) { + mutableVaultStateStateFlow.update { + it.copy( + unlockingVaultUserIds = it.unlockingVaultUserIds + userId, + ) + } + } + + private fun setVaultToNotUnlocking(userId: String) { + mutableVaultStateStateFlow.update { + it.copy( + unlockingVaultUserIds = it.unlockingVaultUserIds - userId, + ) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt new file mode 100644 index 0000000000..2feff8e6fc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt @@ -0,0 +1,30 @@ +package com.x8bit.bitwarden.data.vault.manager.di + +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource +import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.VaultLockManager +import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * Provides managers in the vault package. + */ +@Module +@InstallIn(SingletonComponent::class) +object VaultManagerModule { + + @Provides + @Singleton + fun provideVaultLockManager( + authDiskSource: AuthDiskSource, + vaultSdkSource: VaultSdkSource, + ): VaultLockManager = + VaultLockManagerImpl( + authDiskSource = authDiskSource, + vaultSdkSource = vaultSdkSource, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index 6586f62bff..1f359ac295 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -6,14 +6,14 @@ import com.bitwarden.core.FolderView import com.bitwarden.core.Kdf import com.bitwarden.core.SendView import com.x8bit.bitwarden.data.platform.repository.model.DataState +import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult 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.UpdateCipherResult 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.VaultState -import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.StateFlow * Responsible for managing vault data inside the network layer. */ @Suppress("TooManyFunctions") -interface VaultRepository { +interface VaultRepository : VaultLockManager { /** * Flow that represents the current vault data. @@ -56,11 +56,6 @@ interface VaultRepository { */ val foldersStateFlow: StateFlow>> - /** - * Flow that represents the current vault state. - */ - val vaultStateFlow: StateFlow - /** * Flow that represents the current send data. */ @@ -98,16 +93,6 @@ interface VaultRepository { */ fun getVaultFolderStateFlow(folderId: String): StateFlow> - /** - * Locks the vault for the current user if currently unlocked. - */ - fun lockVaultForCurrentUser() - - /** - * Locks the vault for the user with the given [userId] if necessary. - */ - fun lockVaultIfNecessary(userId: String) - /** * Emits the totp code result flow to listeners. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 43931370d0..8b09e3a1e1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -5,7 +5,6 @@ import com.bitwarden.core.CollectionView import com.bitwarden.core.FolderView import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoMethod -import com.bitwarden.core.InitUserCryptoRequest import com.bitwarden.core.Kdf import com.bitwarden.core.SendView import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource @@ -19,7 +18,6 @@ import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates import com.x8bit.bitwarden.data.platform.repository.util.map import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn import com.x8bit.bitwarden.data.platform.repository.util.updateToPendingOrLoading -import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.flatMap import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson @@ -29,15 +27,14 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource -import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult +import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult 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.UpdateCipherResult 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.VaultState -import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend @@ -46,7 +43,6 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionLi 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.toVaultUnlockResult import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -56,11 +52,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn @@ -84,8 +77,10 @@ class VaultRepositoryImpl( private val vaultDiskSource: VaultDiskSource, private val vaultSdkSource: VaultSdkSource, private val authDiskSource: AuthDiskSource, + private val vaultLockManager: VaultLockManager, dispatcherManager: DispatcherManager, -) : VaultRepository { +) : VaultRepository, + VaultLockManager by vaultLockManager { private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) private val ioScope = CoroutineScope(dispatcherManager.io) @@ -96,14 +91,6 @@ class VaultRepositoryImpl( private val mutableTotpCodeResultFlow = bufferedMutableSharedFlow() - private val mutableVaultStateStateFlow = - MutableStateFlow( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - ) - private val mutableSendDataStateFlow = MutableStateFlow>(DataState.Loading) private val mutableCiphersStateFlow = @@ -151,9 +138,6 @@ class VaultRepositoryImpl( override val collectionsStateFlow: StateFlow>> get() = mutableCollectionsStateFlow.asStateFlow() - override val vaultStateFlow: StateFlow - get() = mutableVaultStateStateFlow.asStateFlow() - override val sendDataStateFlow: StateFlow> get() = mutableSendDataStateFlow.asStateFlow() @@ -276,16 +260,6 @@ class VaultRepositoryImpl( initialValue = DataState.Loading, ) - override fun lockVaultForCurrentUser() { - authDiskSource.userState?.activeUserId?.let { - lockVaultIfNecessary(it) - } - } - - override fun lockVaultIfNecessary(userId: String) { - setVaultToLocked(userId = userId) - } - override fun emitTotpCodeResult(totpCodeResult: TotpCodeResult) { mutableTotpCodeResultFlow.tryEmit(totpCodeResult) } @@ -320,7 +294,7 @@ class VaultRepositoryImpl( privateKey: String, organizationKeys: Map?, ): VaultUnlockResult = - unlockVaultInternal( + unlockVault( userId = userId, email = email, kdf = kdf, @@ -446,46 +420,6 @@ class VaultRepositoryImpl( ) } - // TODO: This is temporary. Eventually this needs to be based on the presence of various - // user keys but this will likely require SDK updates to support this (BIT-1190). - private fun setVaultToUnlocked(userId: String) { - mutableVaultStateStateFlow.update { - it.copy( - unlockedVaultUserIds = it.unlockedVaultUserIds + userId, - ) - } - } - - // TODO: This is temporary. Eventually this needs to be based on the presence of various - // user keys but this will likely require SDK updates to support this (BIT-1190). - private fun setVaultToLocked(userId: String) { - vaultSdkSource.clearCrypto(userId = userId) - mutableVaultStateStateFlow.update { - it.copy( - unlockedVaultUserIds = it.unlockedVaultUserIds - userId, - ) - } - } - - private fun setVaultToUnlocking(userId: String) { - mutableVaultStateStateFlow.update { - it.copy( - unlockingVaultUserIds = it.unlockingVaultUserIds + userId, - ) - } - } - - private fun setVaultToNotUnlocking(userId: String) { - mutableVaultStateStateFlow.update { - it.copy( - unlockingVaultUserIds = it.unlockingVaultUserIds - userId, - ) - } - } - - private fun isVaultUnlocking(userId: String) = - userId in mutableVaultStateStateFlow.value.unlockingVaultUserIds - private fun storeProfileData( syncResponse: SyncResponseJson, ) { @@ -527,7 +461,7 @@ class VaultRepositoryImpl( ?: return VaultUnlockResult.InvalidStateError val organizationKeys = authDiskSource .getOrganizationKeys(userId = userId) - return unlockVaultInternal( + return unlockVault( userId = userId, email = account.profile.email, kdf = account.profile.toSdkParams(), @@ -537,59 +471,6 @@ class VaultRepositoryImpl( ) } - private suspend fun unlockVaultInternal( - userId: String, - email: String, - kdf: Kdf, - privateKey: String, - initUserCryptoMethod: InitUserCryptoMethod, - organizationKeys: Map?, - ): VaultUnlockResult = - flow { - setVaultToUnlocking(userId = userId) - emit( - vaultSdkSource - .initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = initUserCryptoMethod, - ), - ) - .flatMap { result -> - // Initialize the SDK for organizations if necessary - if (organizationKeys != null && - result is InitializeCryptoResult.Success - ) { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = organizationKeys, - ), - ) - } else { - result.asSuccess() - } - } - .fold( - onFailure = { VaultUnlockResult.GenericError }, - onSuccess = { initializeCryptoResult -> - initializeCryptoResult - .toVaultUnlockResult() - .also { - if (it is VaultUnlockResult.Success) { - setVaultToUnlocked(userId = userId) - } - } - }, - ), - ) - } - .onCompletion { setVaultToNotUnlocking(userId = userId) } - .first() - private suspend fun unlockVaultForOrganizationsIfNecessary( syncResponse: SyncResponseJson, ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt index a8ce0cf677..f242f43099 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepositoryImpl import dagger.Module @@ -31,6 +32,7 @@ object VaultRepositoryModule { vaultDiskSource: VaultDiskSource, vaultSdkSource: VaultSdkSource, authDiskSource: AuthDiskSource, + vaultLockManager: VaultLockManager, dispatcherManager: DispatcherManager, ): VaultRepository = VaultRepositoryImpl( syncService = syncService, @@ -39,6 +41,7 @@ object VaultRepositoryModule { vaultDiskSource = vaultDiskSource, vaultSdkSource = vaultSdkSource, authDiskSource = authDiskSource, + vaultLockManager = vaultLockManager, dispatcherManager = dispatcherManager, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt new file mode 100644 index 0000000000..421e77756e --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt @@ -0,0 +1,631 @@ +package com.x8bit.bitwarden.data.vault.manager + +import com.bitwarden.core.InitOrgCryptoRequest +import com.bitwarden.core.InitUserCryptoMethod +import com.bitwarden.core.InitUserCryptoRequest +import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson +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.auth.repository.util.toSdkParams +import com.x8bit.bitwarden.data.platform.util.asFailure +import com.x8bit.bitwarden.data.platform.util.asSuccess +import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult +import com.x8bit.bitwarden.data.vault.repository.model.VaultState +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult +import io.mockk.awaits +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify +import kotlinx.coroutines.async +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class VaultLockManagerTest { + private val fakeAuthDiskSource = FakeAuthDiskSource() + private val vaultSdkSource: VaultSdkSource = mockk { + every { clearCrypto(userId = any()) } just runs + } + + private val vaultLockManager: VaultLockManager = VaultLockManagerImpl( + authDiskSource = fakeAuthDiskSource, + vaultSdkSource = vaultSdkSource, + ) + + @Test + fun `isVaultUnlocked should return the correct value based on the vault lock state`() = + runTest { + val userId = "userId" + assertFalse(vaultLockManager.isVaultUnlocked(userId = userId)) + + verifyUnlockedVault(userId = userId) + + assertTrue(vaultLockManager.isVaultUnlocked(userId = userId)) + } + + @Test + fun `isVaultLocking should return the correct value based on the vault unlocking state`() = + runTest { + val userId = "userId" + assertFalse(vaultLockManager.isVaultUnlocking(userId = userId)) + + val unlockingJob = async { + verifyUnlockingVault(userId = userId) + } + this.testScheduler.advanceUntilIdle() + + assertTrue(vaultLockManager.isVaultUnlocking(userId = userId)) + + unlockingJob.cancel() + this.testScheduler.advanceUntilIdle() + + assertFalse(vaultLockManager.isVaultUnlocking(userId = userId)) + } + + @Test + fun `lockVaultIfNecessary should lock the given account if it is currently unlocked`() = + runTest { + val userId = "userId" + verifyUnlockedVault(userId = userId) + + assertEquals( + VaultState( + unlockedVaultUserIds = setOf(userId), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + + vaultLockManager.lockVaultIfNecessary(userId = userId) + + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + verify { vaultSdkSource.clearCrypto(userId = userId) } + } + + @Suppress("MaxLineLength") + @Test + fun `lockVaultForCurrentUser should lock the vault for the current user if it is currently unlocked`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + verifyUnlockedVault(userId = userId) + + assertEquals( + VaultState( + unlockedVaultUserIds = setOf(userId), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + + vaultLockManager.lockVaultForCurrentUser() + + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + verify { vaultSdkSource.clearCrypto(userId = userId) } + } + + @Test + fun `unlockVault with initializeCrypto success should return Success`() = runTest { + val userId = "userId" + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = mapOf("orgId1" to "orgKey1") + coEvery { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + userId = userId, + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } returns InitializeCryptoResult.Success.asSuccess() + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + + val result = vaultLockManager.unlockVault( + userId = userId, + kdf = kdf, + email = email, + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + privateKey = privateKey, + organizationKeys = organizationKeys, + ) + + assertEquals(VaultUnlockResult.Success, result) + assertEquals( + VaultState( + unlockedVaultUserIds = setOf(userId), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } + coVerify(exactly = 1) { + vaultSdkSource.initializeOrganizationCrypto( + userId = userId, + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVault with initializeCrypto authentication failure for users should return AuthenticationError`() = + runTest { + val userId = "userId" + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = mapOf("orgId1" to "orgKey1") + coEvery { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } returns InitializeCryptoResult.AuthenticationError.asSuccess() + + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + val result = vaultLockManager.unlockVault( + userId = userId, + kdf = kdf, + email = email, + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + privateKey = privateKey, + organizationKeys = organizationKeys, + ) + + assertEquals(VaultUnlockResult.AuthenticationError, result) + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVault with initializeCrypto authentication failure for orgs should return AuthenticationError`() = + runTest { + val userId = "userId" + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = mapOf("orgId1" to "orgKey1") + coEvery { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + userId = userId, + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } returns InitializeCryptoResult.AuthenticationError.asSuccess() + + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + + val result = vaultLockManager.unlockVault( + userId = userId, + kdf = kdf, + email = email, + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + privateKey = privateKey, + organizationKeys = organizationKeys, + ) + + assertEquals(VaultUnlockResult.AuthenticationError, result) + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } + coVerify(exactly = 1) { + vaultSdkSource.initializeOrganizationCrypto( + userId = userId, + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } + } + + @Test + fun `unlockVault with initializeCrypto failure for users should return GenericError`() = + runTest { + val userId = "userId" + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = mapOf("orgId1" to "orgKey1") + coEvery { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } returns Throwable("Fail").asFailure() + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + + val result = vaultLockManager.unlockVault( + userId = userId, + kdf = kdf, + email = email, + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + privateKey = privateKey, + organizationKeys = organizationKeys, + ) + + assertEquals(VaultUnlockResult.GenericError, result) + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } + } + + @Test + fun `unlockVault with initializeCrypto failure for orgs should return GenericError`() = + runTest { + val userId = "userId" + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = mapOf("orgId1" to "orgKey1") + coEvery { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + userId = userId, + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } returns Throwable("Fail").asFailure() + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + + val result = vaultLockManager.unlockVault( + userId = userId, + kdf = kdf, + email = email, + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + privateKey = privateKey, + organizationKeys = organizationKeys, + ) + + assertEquals(VaultUnlockResult.GenericError, result) + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + vaultLockManager.vaultStateFlow.value, + ) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } + coVerify(exactly = 1) { + vaultSdkSource.initializeOrganizationCrypto( + userId = userId, + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } + } + + /** + * Helper to ensures that the vault for the user with the given [userId] is actively unlocking. + * Note that this call will actively hang. + */ + private suspend fun verifyUnlockingVault(userId: String) { + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = null + coEvery { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } just awaits + + vaultLockManager.unlockVault( + userId = userId, + kdf = kdf, + email = email, + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + organizationKeys = organizationKeys, + ) + } + + /** + * Helper to ensures that the vault for the user with the given [userId] is unlocked. + */ + private suspend fun verifyUnlockedVault(userId: String) { + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = null + coEvery { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + + val result = vaultLockManager.unlockVault( + userId = userId, + kdf = kdf, + email = email, + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + organizationKeys = organizationKeys, + ) + + assertEquals(VaultUnlockResult.Success, result) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + userId = userId, + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } + } +} + +private val MOCK_PROFILE = AccountJson.Profile( + userId = "mockId-1", + email = "email", + isEmailVerified = true, + name = null, + stamp = null, + organizationId = null, + avatarColorHex = null, + hasPremium = false, + forcePasswordResetReason = null, + kdfType = null, + kdfIterations = null, + kdfMemory = null, + kdfParallelism = null, + userDecryptionOptions = null, +) + +private val MOCK_ACCOUNT = AccountJson( + profile = MOCK_PROFILE, + tokens = AccountJson.Tokens( + 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, + ), +) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 9930abfce7..f498fa374e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -45,6 +45,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCollecti import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFolder 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.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.SendData @@ -65,11 +66,8 @@ import io.mockk.just import io.mockk.mockk import io.mockk.runs import io.mockk.verify -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -87,6 +85,21 @@ class VaultRepositoryTest { private val vaultSdkSource: VaultSdkSource = mockk { every { clearCrypto(userId = any()) } just runs } + private val mutableVaultStateFlow = MutableStateFlow( + VaultState( + unlockedVaultUserIds = emptySet(), + unlockingVaultUserIds = emptySet(), + ), + ) + private val vaultLockManager: VaultLockManager = mockk { + every { vaultStateFlow } returns mutableVaultStateFlow + every { isVaultUnlocked(any()) } returns false + every { isVaultUnlocking(any()) } returns false + every { lockVault(any()) } just runs + every { lockVaultIfNecessary(any()) } just runs + every { lockVaultForCurrentUser() } just runs + } + private val vaultRepository = VaultRepositoryImpl( syncService = syncService, sendsService = sendsService, @@ -94,6 +107,7 @@ class VaultRepositoryTest { vaultDiskSource = vaultDiskSource, vaultSdkSource = vaultSdkSource, authDiskSource = fakeAuthDiskSource, + vaultLockManager = vaultLockManager, dispatcherManager = dispatcherManager, ) @@ -529,61 +543,21 @@ class VaultRepositoryTest { @Suppress("MaxLineLength") @Test - fun `lockVaultForCurrentUser should lock the vault for the current user if it is currently unlocked`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - verifyUnlockedVault(userId = userId) - - assertEquals( - VaultState( - unlockedVaultUserIds = setOf(userId), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - - vaultRepository.lockVaultForCurrentUser() - - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - verify { vaultSdkSource.clearCrypto(userId = userId) } - } + fun `lockVaultForCurrentUser should delegate to the VaultLockManager`() { + vaultRepository.lockVaultForCurrentUser() + verify { vaultLockManager.lockVaultForCurrentUser() } + } @Test - fun `lockVaultIfNecessary should lock the given account if it is currently unlocked`() = - runTest { - val userId = "userId" - verifyUnlockedVault(userId = userId) - - assertEquals( - VaultState( - unlockedVaultUserIds = setOf(userId), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - - vaultRepository.lockVaultIfNecessary(userId = userId) - - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - verify { vaultSdkSource.clearCrypto(userId = userId) } - } + fun `lockVaultIfNecessary should delete to the VaultLockManager`() { + val userId = "userId" + vaultRepository.lockVaultIfNecessary(userId = userId) + verify { vaultLockManager.lockVaultIfNecessary(userId = userId) } + } @Suppress("MaxLineLength") @Test - fun `unlockVaultAndSyncForCurrentUser with unlockVault Success should sync and return Success`() = + fun `unlockVaultAndSyncForCurrentUser with VaultLockManager Success should unlock for the current user, sync, and return Success`() = runTest { val userId = "mockId-1" val mockSyncResponse = createMockSyncResponse(number = 1) @@ -621,48 +595,183 @@ class VaultRepositoryTest { organizationKeys = createMockOrganizationKeys(number = 1), ) fakeAuthDiskSource.userState = MOCK_USER_STATE + val mockVaultUnlockResult = VaultUnlockResult.Success coEvery { - vaultSdkSource.initializeCrypto( + vaultLockManager.unlockVault( userId = userId, - request = InitUserCryptoRequest( - kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), - email = "email", - privateKey = "mockPrivateKey-1", - method = InitUserCryptoMethod.Password( - password = "mockPassword-1", - userKey = "mockKey-1", - ), + kdf = MOCK_PROFILE.toSdkParams(), + email = "email", + privateKey = "mockPrivateKey-1", + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = "mockPassword-1", + userKey = "mockKey-1", ), + + organizationKeys = createMockOrganizationKeys(number = 1), ) - } returns Result.success(InitializeCryptoResult.Success) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) + } returns mockVaultUnlockResult val result = vaultRepository.unlockVaultAndSyncForCurrentUser( masterPassword = "mockPassword-1", ) assertEquals( - VaultUnlockResult.Success, + mockVaultUnlockResult, result, ) - assertEquals( - VaultState( - unlockedVaultUserIds = setOf("mockId-1"), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) coVerify { syncService.sync() } + coVerify { + vaultLockManager.unlockVault( + userId = userId, + kdf = MOCK_PROFILE.toSdkParams(), + email = "email", + privateKey = "mockPrivateKey-1", + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = "mockPassword-1", + userKey = "mockKey-1", + ), + + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVaultAndSyncForCurrentUser with VaultLockManager non-Success should unlock for the current user and return the error`() = + runTest { + val userId = "mockId-1" + val mockSyncResponse = createMockSyncResponse(number = 1) + coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + userId = userId, + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + coEvery { + vaultDiskSource.replaceVaultData( + userId = MOCK_USER_STATE.activeUserId, + vault = mockSyncResponse, + ) + } just runs + coEvery { + vaultSdkSource.decryptSendList( + userId = userId, + sendList = listOf(createMockSdkSend(number = 1)), + ) + } returns listOf(createMockSendView(number = 1)).asSuccess() + fakeAuthDiskSource.storePrivateKey( + userId = "mockId-1", + privateKey = "mockPrivateKey-1", + ) + fakeAuthDiskSource.storeUserKey( + userId = "mockId-1", + userKey = "mockKey-1", + ) + fakeAuthDiskSource.storeOrganizationKeys( + userId = "mockId-1", + organizationKeys = createMockOrganizationKeys(number = 1), + ) + fakeAuthDiskSource.userState = MOCK_USER_STATE + val mockVaultUnlockResult = VaultUnlockResult.InvalidStateError + coEvery { + vaultLockManager.unlockVault( + userId = userId, + kdf = MOCK_PROFILE.toSdkParams(), + email = "email", + privateKey = "mockPrivateKey-1", + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = "mockPassword-1", + userKey = "mockKey-1", + ), + + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } returns mockVaultUnlockResult + + val result = vaultRepository.unlockVaultAndSyncForCurrentUser( + masterPassword = "mockPassword-1", + ) + + assertEquals( + mockVaultUnlockResult, + result, + ) + coVerify(exactly = 0) { syncService.sync() } + coVerify { + vaultLockManager.unlockVault( + userId = userId, + kdf = MOCK_PROFILE.toSdkParams(), + email = "email", + privateKey = "mockPrivateKey-1", + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = "mockPassword-1", + userKey = "mockKey-1", + ), + + organizationKeys = createMockOrganizationKeys(number = 1), + ) + } } @Test - fun `sync should be able to be called after unlockVaultAndSyncForCurrentUser is canceled`() = + fun `unlockVault should delegate to the VaultLockManager`() = runTest { + val userId = "userId" + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = mapOf("orgId1" to "orgKey1") + val mockVaultUnlockResult = mockk() + coEvery { + vaultLockManager.unlockVault( + userId = userId, + kdf = kdf, + email = email, + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + + organizationKeys = organizationKeys, + ) + } returns mockVaultUnlockResult + + val result = vaultRepository.unlockVault( + userId = userId, + masterPassword = masterPassword, + kdf = kdf, + email = email, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationKeys, + ) + + assertEquals(mockVaultUnlockResult, result) + coVerify(exactly = 1) { + vaultLockManager.unlockVault( + userId = userId, + kdf = kdf, + email = email, + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + + organizationKeys = organizationKeys, + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `sync should not be able to be called while isVaultUnlocking is true for the current user`() = runTest { val userId = "mockId-1" val mockSyncResponse = createMockSyncResponse(number = 1) @@ -711,298 +820,21 @@ class VaultRepositoryTest { ) } just awaits - val scope = CoroutineScope(Dispatchers.Unconfined) - scope.launch { - vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "mockPassword-1") - } - coVerify(exactly = 0) { syncService.sync() } - scope.cancel() - vaultRepository.sync() + every { + vaultLockManager.isVaultUnlocking(userId) + } returns true - coVerify(exactly = 1) { syncService.sync() } - } - - @Test - fun `sync should not be able to be called while unlockVaultAndSyncForCurrentUser is called`() = - runTest { - coEvery { - syncService.sync() - } returns Result.success(createMockSyncResponse(number = 1)) - fakeAuthDiskSource.storePrivateKey( - userId = "mockId-1", - privateKey = "mockPrivateKey-1", - ) - fakeAuthDiskSource.storeUserKey( - userId = "mockId-1", - userKey = "mockKey-1", - ) - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), - email = "email", - privateKey = "mockPrivateKey-1", - method = InitUserCryptoMethod.Password( - password = "mockPassword-1", - userKey = "mockKey-1", - ), - ), - ) - } just awaits - - val scope = CoroutineScope(Dispatchers.Unconfined) - scope.launch { - vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "mockPassword-1") - } // We call sync here but the call to the SyncService should be blocked - // by the active call to unlockVaultAndSync + // by unlocking vault. vaultRepository.sync() - - scope.cancel() - coVerify(exactly = 0) { syncService.sync() } - } - @Suppress("MaxLineLength") - @Test - fun `unlockVaultAndSyncForCurrentUser with unlockVault failure for users should return GenericError`() = - runTest { - coEvery { - syncService.sync() - } returns Result.success(createMockSyncResponse(number = 1)) - fakeAuthDiskSource.storePrivateKey( - userId = "mockId-1", - privateKey = "mockPrivateKey-1", - ) - fakeAuthDiskSource.storeUserKey( - userId = "mockId-1", - userKey = "mockKey-1", - ) - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), - email = "email", - privateKey = "mockPrivateKey-1", - method = InitUserCryptoMethod.Password( - password = "mockPassword-1", - userKey = "mockKey-1", - ), - ), - ) - } returns Result.failure(IllegalStateException()) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) + every { + vaultLockManager.isVaultUnlocking(userId) + } returns false - val result = vaultRepository.unlockVaultAndSyncForCurrentUser( - masterPassword = "mockPassword-1", - ) - - assertEquals( - VaultUnlockResult.GenericError, - result, - ) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - } - - @Suppress("MaxLineLength") - @Test - fun `unlockVaultAndSyncForCurrentUser with unlockVault failure for orgs should return GenericError`() = - runTest { - coEvery { - syncService.sync() - } returns Result.success(createMockSyncResponse(number = 1)) - fakeAuthDiskSource.storePrivateKey( - userId = "mockId-1", - privateKey = "mockPrivateKey-1", - ) - fakeAuthDiskSource.storeUserKey( - userId = "mockId-1", - userKey = "mockKey-1", - ) - fakeAuthDiskSource.storeOrganizationKeys( - userId = "mockId-1", - organizationKeys = createMockOrganizationKeys(number = 1), - ) - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), - email = "email", - privateKey = "mockPrivateKey-1", - method = InitUserCryptoMethod.Password( - password = "mockPassword-1", - userKey = "mockKey-1", - ), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns IllegalStateException().asFailure() - - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - - val result = vaultRepository.unlockVaultAndSyncForCurrentUser( - masterPassword = "mockPassword-1", - ) - - assertEquals( - VaultUnlockResult.GenericError, - result, - ) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - } - - @Suppress("MaxLineLength") - @Test - fun `unlockVaultAndSyncForCurrentUser with unlockVault AuthenticationError for users should return AuthenticationError`() = - runTest { - coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) - fakeAuthDiskSource.storePrivateKey( - userId = "mockId-1", - privateKey = "mockPrivateKey-1", - ) - fakeAuthDiskSource.storeUserKey( - userId = "mockId-1", - userKey = "mockKey-1", - ) - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), - email = "email", - privateKey = "mockPrivateKey-1", - method = InitUserCryptoMethod.Password( - password = "", - userKey = "mockKey-1", - ), - ), - ) - } returns Result.success(InitializeCryptoResult.AuthenticationError) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - - val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "") - assertEquals( - VaultUnlockResult.AuthenticationError, - result, - ) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - } - - @Suppress("MaxLineLength") - @Test - fun `unlockVaultAndSyncForCurrentUser with unlockVault AuthenticationError for orgs should return AuthenticationError`() = - runTest { - coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) - fakeAuthDiskSource.storePrivateKey( - userId = "mockId-1", - privateKey = "mockPrivateKey-1", - ) - fakeAuthDiskSource.storeUserKey( - userId = "mockId-1", - userKey = "mockKey-1", - ) - fakeAuthDiskSource.storeOrganizationKeys( - userId = "mockId-1", - organizationKeys = createMockOrganizationKeys(number = 1), - ) - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), - email = "email", - privateKey = "mockPrivateKey-1", - method = InitUserCryptoMethod.Password( - password = "", - userKey = "mockKey-1", - ), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.AuthenticationError.asSuccess() - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - - val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "") - assertEquals( - VaultUnlockResult.AuthenticationError, - result, - ) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) + vaultRepository.sync() + coVerify(exactly = 1) { syncService.sync() } } @Suppress("MaxLineLength") @@ -1093,117 +925,6 @@ class VaultRepositoryTest { VaultUnlockResult.InvalidStateError, result, ) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - } - - @Test - fun `unlockVault with initializeCrypto success should return Success`() = runTest { - val userId = "userId" - val kdf = MOCK_PROFILE.toSdkParams() - val email = MOCK_PROFILE.email - val masterPassword = "drowssap" - val userKey = "12345" - val privateKey = "54321" - val organizationKeys = mapOf("orgId1" to "orgKey1") - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest(organizationKeys = organizationKeys), - ) - } returns InitializeCryptoResult.Success.asSuccess() - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - - val result = vaultRepository.unlockVault( - userId = userId, - masterPassword = masterPassword, - kdf = kdf, - email = email, - userKey = userKey, - privateKey = privateKey, - organizationKeys = organizationKeys, - ) - - assertEquals(VaultUnlockResult.Success, result) - assertEquals( - VaultState( - unlockedVaultUserIds = setOf(userId), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - coVerify(exactly = 1) { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } - coVerify(exactly = 1) { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest(organizationKeys = organizationKeys), - ) - } - } - - @Suppress("MaxLineLength") - @Test - fun `unlockVault with initializeCrypto authentication failure for users should return AuthenticationError`() = - runTest { - val userId = "userId" - val kdf = MOCK_PROFILE.toSdkParams() - val email = MOCK_PROFILE.email - val masterPassword = "drowssap" - val userKey = "12345" - val privateKey = "54321" - val organizationKeys = mapOf("orgId1" to "orgKey1") - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } returns InitializeCryptoResult.AuthenticationError.asSuccess() assertEquals( VaultState( @@ -1212,332 +933,8 @@ class VaultRepositoryTest { ), vaultRepository.vaultStateFlow.value, ) - - val result = vaultRepository.unlockVault( - userId = userId, - masterPassword = masterPassword, - kdf = kdf, - email = email, - userKey = userKey, - privateKey = privateKey, - organizationKeys = organizationKeys, - ) - - assertEquals(VaultUnlockResult.AuthenticationError, result) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - coVerify(exactly = 1) { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } } - @Suppress("MaxLineLength") - @Test - fun `unlockVault with initializeCrypto authentication failure for orgs should return AuthenticationError`() = - runTest { - val userId = "userId" - val kdf = MOCK_PROFILE.toSdkParams() - val email = MOCK_PROFILE.email - val masterPassword = "drowssap" - val userKey = "12345" - val privateKey = "54321" - val organizationKeys = mapOf("orgId1" to "orgKey1") - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest(organizationKeys = organizationKeys), - ) - } returns InitializeCryptoResult.AuthenticationError.asSuccess() - - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - - val result = vaultRepository.unlockVault( - userId = userId, - masterPassword = masterPassword, - kdf = kdf, - email = email, - userKey = userKey, - privateKey = privateKey, - organizationKeys = organizationKeys, - ) - - assertEquals(VaultUnlockResult.AuthenticationError, result) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - coVerify(exactly = 1) { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } - coVerify(exactly = 1) { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest(organizationKeys = organizationKeys), - ) - } - } - - @Test - fun `unlockVault with initializeCrypto failure for users should return GenericError`() = - runTest { - val userId = "userId" - val kdf = MOCK_PROFILE.toSdkParams() - val email = MOCK_PROFILE.email - val masterPassword = "drowssap" - val userKey = "12345" - val privateKey = "54321" - val organizationKeys = mapOf("orgId1" to "orgKey1") - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } returns Throwable("Fail").asFailure() - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - - val result = vaultRepository.unlockVault( - userId = userId, - masterPassword = masterPassword, - kdf = kdf, - email = email, - userKey = userKey, - privateKey = privateKey, - organizationKeys = organizationKeys, - ) - - assertEquals(VaultUnlockResult.GenericError, result) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - coVerify(exactly = 1) { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } - } - - @Test - fun `unlockVault with initializeCrypto failure for orgs should return GenericError`() = - runTest { - val userId = "userId" - val kdf = MOCK_PROFILE.toSdkParams() - val email = MOCK_PROFILE.email - val masterPassword = "drowssap" - val userKey = "12345" - val privateKey = "54321" - val organizationKeys = mapOf("orgId1" to "orgKey1") - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest(organizationKeys = organizationKeys), - ) - } returns Throwable("Fail").asFailure() - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - - val result = vaultRepository.unlockVault( - userId = userId, - masterPassword = masterPassword, - kdf = kdf, - email = email, - userKey = userKey, - privateKey = privateKey, - organizationKeys = organizationKeys, - ) - - assertEquals(VaultUnlockResult.GenericError, result) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - coVerify(exactly = 1) { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } - coVerify(exactly = 1) { - vaultSdkSource.initializeOrganizationCrypto( - userId = userId, - request = InitOrgCryptoRequest(organizationKeys = organizationKeys), - ) - } - } - - @Test - fun `unlockVault with initializeCrypto awaiting should block calls to sync`() = runTest { - val userId = "userId" - val kdf = MOCK_PROFILE.toSdkParams() - val email = MOCK_PROFILE.email - val masterPassword = "drowssap" - val userKey = "12345" - val privateKey = "54321" - val organizationKeys = null - coEvery { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } just awaits - - val scope = CoroutineScope(Dispatchers.Unconfined) - scope.launch { - vaultRepository.unlockVault( - userId = userId, - masterPassword = masterPassword, - kdf = kdf, - email = email, - userKey = userKey, - privateKey = privateKey, - organizationKeys = organizationKeys, - ) - } - - // The given userId is marked as unlocking - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - unlockingVaultUserIds = setOf(userId), - ), - vaultRepository.vaultStateFlow.value, - ) - - // Does nothing because we are blocking - vaultRepository.sync() - scope.cancel() - - coVerify(exactly = 0) { syncService.sync() } - coVerify(exactly = 1) { - vaultSdkSource.initializeCrypto( - userId = userId, - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, - ), - ), - ) - } - } - @Test fun `clearUnlockedData should update the vaultDataStateFlow to Loading`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE @@ -1631,24 +1028,25 @@ class VaultRepositoryTest { } @Test - fun `getVaultItemStateFlow should update to Error when a sync fails generically`() = runTest { - val folderId = 1234 - val folderIdString = "mockId-$folderId" - val throwable = Throwable("Fail") - fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { syncService.sync() } returns throwable.asFailure() - setupVaultDiskSourceFlows() + fun `getVaultItemStateFlow should update to Error when a sync fails generically`() = + runTest { + val folderId = 1234 + val folderIdString = "mockId-$folderId" + val throwable = Throwable("Fail") + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { syncService.sync() } returns throwable.asFailure() + setupVaultDiskSourceFlows() - vaultRepository.getVaultItemStateFlow(folderIdString).test { - assertEquals(DataState.Loading, awaitItem()) - vaultRepository.sync() - assertEquals(DataState.Error(throwable), awaitItem()) - } + vaultRepository.getVaultItemStateFlow(folderIdString).test { + assertEquals(DataState.Loading, awaitItem()) + vaultRepository.sync() + assertEquals(DataState.Error(throwable), awaitItem()) + } - coVerify(exactly = 1) { - syncService.sync() + coVerify(exactly = 1) { + syncService.sync() + } } - } @Test fun `getVaultItemStateFlow should update to NoNetwork when a sync fails from no network`() = @@ -1691,24 +1089,25 @@ class VaultRepositoryTest { } @Test - fun `getVaultFolderStateFlow should update to Error when a sync fails generically`() = runTest { - val folderId = 1234 - val folderIdString = "mockId-$folderId" - val throwable = Throwable("Fail") - fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { syncService.sync() } returns throwable.asFailure() - setupVaultDiskSourceFlows() + fun `getVaultFolderStateFlow should update to Error when a sync fails generically`() = + runTest { + val folderId = 1234 + val folderIdString = "mockId-$folderId" + val throwable = Throwable("Fail") + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { syncService.sync() } returns throwable.asFailure() + setupVaultDiskSourceFlows() - vaultRepository.getVaultFolderStateFlow(folderIdString).test { - assertEquals(DataState.Loading, awaitItem()) - vaultRepository.sync() - assertEquals(DataState.Error(throwable), awaitItem()) - } + vaultRepository.getVaultFolderStateFlow(folderIdString).test { + assertEquals(DataState.Loading, awaitItem()) + vaultRepository.sync() + assertEquals(DataState.Error(throwable), awaitItem()) + } - coVerify(exactly = 1) { - syncService.sync() + coVerify(exactly = 1) { + syncService.sync() + } } - } @Test fun `createCipher with encryptCipher failure should return CreateCipherResult failure`() = @@ -1900,7 +1299,12 @@ class VaultRepositoryTest { } returns UpdateCipherResponseJson .Success(cipher = mockCipher) .asSuccess() - coEvery { vaultDiskSource.saveCipher(userId = userId, cipher = mockCipher) } just runs + coEvery { + vaultDiskSource.saveCipher( + userId = userId, + cipher = mockCipher, + ) + } just runs val result = vaultRepository.updateCipher( cipherId = cipherId, @@ -1911,18 +1315,19 @@ class VaultRepositoryTest { } @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) - coEvery { - vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) - } returns IllegalStateException().asFailure() + fun `createSend with encryptSend failure should return CreateSendResult failure`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val mockSendView = createMockSendView(number = 1) + coEvery { + vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) + } returns IllegalStateException().asFailure() - val result = vaultRepository.createSend(sendView = mockSendView) + val result = vaultRepository.createSend(sendView = mockSendView) - assertEquals(CreateSendResult.Error, result) - } + assertEquals(CreateSendResult.Error, result) + } @Test @Suppress("MaxLineLength") @@ -1969,22 +1374,23 @@ class VaultRepositoryTest { } @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) - coEvery { - vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) - } returns IllegalStateException().asFailure() + 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) + coEvery { + vaultSdkSource.encryptSend(userId = userId, sendView = mockSendView) + } returns IllegalStateException().asFailure() - val result = vaultRepository.updateSend( - sendId = sendId, - sendView = mockSendView, - ) + val result = vaultRepository.updateSend( + sendId = sendId, + sendView = mockSendView, + ) - assertEquals(UpdateSendResult.Error(errorMessage = null), result) - } + assertEquals(UpdateSendResult.Error(errorMessage = null), result) + } @Test @Suppress("MaxLineLength")