diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index 8be23db75be..ca9df21ca2e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -94,6 +94,7 @@ import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource +import com.x8bit.bitwarden.data.vault.manager.VaultDataManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager @@ -132,7 +133,7 @@ object PlatformManagerModule { fun provideOrganizationEventManager( authStateManager: AuthStateManager, organizationManager: OrganizationManager, - vaultRepository: VaultRepository, + vaultDataManager: VaultDataManager, authDiskSource: AuthDiskSource, clock: Clock, dispatcherManager: DispatcherManager, @@ -142,7 +143,7 @@ object PlatformManagerModule { authStateManager = authStateManager, authDiskSource = authDiskSource, organizationManager = organizationManager, - vaultRepository = vaultRepository, + vaultDataManager = vaultDataManager, clock = clock, dispatcherManager = dispatcherManager, eventDiskSource = eventDiskSource, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/event/OrganizationEventManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/event/OrganizationEventManagerImpl.kt index f864ec414e7..025f4e97121 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/event/OrganizationEventManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/event/OrganizationEventManagerImpl.kt @@ -12,7 +12,7 @@ import com.x8bit.bitwarden.data.auth.manager.OrganizationManager import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent -import com.x8bit.bitwarden.data.vault.repository.VaultRepository +import com.x8bit.bitwarden.data.vault.manager.VaultDataManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -41,7 +41,7 @@ internal class OrganizationEventManagerImpl( private val authStateManager: AuthStateManager, private val authDiskSource: AuthDiskSource, private val organizationManager: OrganizationManager, - private val vaultRepository: VaultRepository, + private val vaultDataManager: VaultDataManager, private val eventDiskSource: EventDiskSource, private val eventService: EventService, dispatcherManager: DispatcherManager, @@ -70,7 +70,7 @@ internal class OrganizationEventManagerImpl( ioScope.launch { event.cipherId?.let { id -> - val cipherOrganizationId = vaultRepository + val cipherOrganizationId = vaultDataManager .getVaultItemStateFlow(itemId = id) .first { it.data != null } .data diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultDataManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultDataManager.kt new file mode 100644 index 00000000000..d6bbe5421c8 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultDataManager.kt @@ -0,0 +1,64 @@ +package com.x8bit.bitwarden.data.vault.manager + +import com.bitwarden.core.data.repository.model.DataState +import com.bitwarden.send.SendView +import com.bitwarden.vault.CipherListView +import com.bitwarden.vault.CipherView +import com.bitwarden.vault.FolderView +import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem +import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType +import kotlinx.coroutines.flow.StateFlow + +/** + * A helper manager for accessing and manging the vault data. + */ +interface VaultDataManager { + /** + * The [VaultFilterType] for the current user. + * + * Note that this does not affect the data provided by the repository and can be used by + * the UI for consistent filtering across screens. + */ + var vaultFilterType: VaultFilterType + + /** + * Flow that represents the data for a single verification code item. + * This may emit null if any issues arise during code generation. + */ + fun getAuthCodeFlow(cipherId: String): StateFlow> + + /** + * Flow that represents the data for the TOTP verification codes for ciphers items. + * This may emit an empty list if any issues arise during code generation. + */ + fun getAuthCodesFlow(): StateFlow>> + + /** + * Completely remove any persisted data from the vault. + */ + fun deleteVaultData(userId: String) + + /** + * Flow that represents the data for a specific vault item as found by ID. This may emit `null` + * if the item cannot be found. + */ + fun getVaultItemStateFlow(itemId: String): StateFlow> + + /** + * Flow that represents the data for a specific vault folder as found by ID. This may emit + * `null` if the folder cannot be found. + */ + fun getVaultFolderStateFlow(folderId: String): StateFlow> + + /** + * Flow that represents the data for a specific send as found by ID. This may emit `null` if + * the Send cannot be found. + */ + fun getSendStateFlow(sendId: String): StateFlow> + + /** + * Flow that represents the data for a specific vault list item as found by ID. This may emit + * `null` if the item cannot be found. + */ + fun getVaultListItemStateFlow(itemId: String): StateFlow> +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultDataManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultDataManagerImpl.kt new file mode 100644 index 00000000000..8b6e206ef01 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultDataManagerImpl.kt @@ -0,0 +1,203 @@ +package com.x8bit.bitwarden.data.vault.manager + +import com.bitwarden.core.data.manager.dispatcher.DispatcherManager +import com.bitwarden.core.data.repository.model.DataState +import com.bitwarden.core.data.repository.util.combineDataStates +import com.bitwarden.core.data.repository.util.map +import com.bitwarden.core.data.repository.util.mapNullable +import com.bitwarden.send.SendView +import com.bitwarden.vault.CipherListView +import com.bitwarden.vault.CipherListViewType +import com.bitwarden.vault.CipherView +import com.bitwarden.vault.FolderView +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource +import com.x8bit.bitwarden.data.autofill.util.login +import com.x8bit.bitwarden.data.platform.util.isActive +import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource +import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult +import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem +import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType +import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** + * The default implementation of the [VaultDataManager]. + */ +internal class VaultDataManagerImpl( + private val authDiskSource: AuthDiskSource, + private val cipherManager: CipherManager, + private val totpCodeManager: TotpCodeManager, + private val vaultDiskSource: VaultDiskSource, + private val vaultSyncManager: VaultSyncManager, + dispatcherManager: DispatcherManager, +) : VaultDataManager { + private val ioScope = CoroutineScope(context = dispatcherManager.io) + private val unconfinedScope = CoroutineScope(context = dispatcherManager.unconfined) + private val activeUserId: String? get() = authDiskSource.userState?.activeUserId + + override var vaultFilterType: VaultFilterType = VaultFilterType.AllVaults + + @OptIn(ExperimentalCoroutinesApi::class) + override fun getAuthCodesFlow(): StateFlow>> { + val userId = activeUserId ?: return MutableStateFlow( + DataState.Error(error = IllegalStateException("No active user"), null), + ) + return vaultSyncManager + .vaultDataStateFlow + .map { dataState -> + dataState.map { vaultData -> + vaultData + .decryptCipherListResult + .successes + .filter { + it.type is CipherListViewType.Login && + !it.login?.totp.isNullOrBlank() && + it.isActive + } + .toFilteredList(vaultFilterType = vaultFilterType) + } + } + .flatMapLatest { cipherDataState -> + val cipherList = cipherDataState.data ?: emptyList() + totpCodeManager + .getTotpCodesForCipherListViewsStateFlow( + userId = userId, + cipherListViews = cipherList, + ) + .map { verificationCodeDataStates -> + combineDataStates( + dataState1 = verificationCodeDataStates, + dataState2 = cipherDataState, + ) { verificationCodeItems, _ -> + // Just return the verification items; we are only combining the + // DataStates to know the overall state. + verificationCodeItems + } + } + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.WhileSubscribed(), + initialValue = DataState.Loading, + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun getAuthCodeFlow(cipherId: String): StateFlow> { + val userId = activeUserId ?: return MutableStateFlow( + DataState.Error(error = IllegalStateException("No active user"), null), + ) + return this.getVaultListItemStateFlow(cipherId) + .flatMapLatest { cipherDataState -> + cipherDataState + .data + ?.let { + totpCodeManager + .getTotpCodeStateFlow(userId = userId, cipherListView = it) + .map { totpCodeDataState -> + combineDataStates( + dataState1 = totpCodeDataState, + dataState2 = cipherDataState, + ) { _, _ -> + // We are only combining the DataStates to know the overall + // state, we map it to the appropriate value below. + } + .mapNullable { totpCodeDataState.data } + } + } + ?: flowOf(DataState.Loaded(data = null)) + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.WhileSubscribed(), + initialValue = DataState.Loading, + ) + } + + override fun deleteVaultData(userId: String) { + ioScope.launch { + vaultDiskSource.deleteVaultData(userId = userId) + } + } + + override fun getVaultItemStateFlow( + itemId: String, + ): StateFlow> = + vaultSyncManager + .vaultDataStateFlow + .map { dataState -> + dataState.map { vaultData -> + val getCipherResult = vaultData + .decryptCipherListResult + .successes + .find { it.id == itemId } + .let { cipherManager.getCipher(cipherId = itemId) } + when (getCipherResult) { + is GetCipherResult.Success -> getCipherResult.cipherView + else -> null + } + } + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.Lazily, + initialValue = DataState.Loading, + ) + + override fun getVaultListItemStateFlow( + itemId: String, + ): StateFlow> = + vaultSyncManager + .vaultDataStateFlow + .map { dataState -> + dataState.map { vaultData -> + vaultData.decryptCipherListResult.successes.find { it.id == itemId } + } + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.Lazily, + initialValue = DataState.Loading, + ) + + override fun getVaultFolderStateFlow( + folderId: String, + ): StateFlow> = + vaultSyncManager + .vaultDataStateFlow + .map { dataState -> + dataState.map { vaultData -> + vaultData.folderViewList.find { it.id == folderId } + } + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.Lazily, + initialValue = DataState.Loading, + ) + + override fun getSendStateFlow( + sendId: String, + ): StateFlow> = + vaultSyncManager + .sendDataStateFlow + .map { dataState -> + dataState.map { sendData -> + sendData.sendViewList.find { it.id == sendId } + } + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.Lazily, + initialValue = DataState.Loading, + ) +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt index 29b9e1c7968..63dbe470ba1 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/di/VaultManagerModule.kt @@ -43,6 +43,8 @@ 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.VaultDataManager +import com.x8bit.bitwarden.data.vault.manager.VaultDataManagerImpl import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager @@ -228,6 +230,24 @@ object VaultManagerModule { clock = clock, ) + @Provides + @Singleton + fun provideVaultDataManager( + authDiskSource: AuthDiskSource, + cipherManager: CipherManager, + totpCodeManager: TotpCodeManager, + vaultDiskSource: VaultDiskSource, + vaultSyncManager: VaultSyncManager, + dispatcherManager: DispatcherManager, + ): VaultDataManager = VaultDataManagerImpl( + authDiskSource = authDiskSource, + cipherManager = cipherManager, + totpCodeManager = totpCodeManager, + vaultDiskSource = vaultDiskSource, + vaultSyncManager = vaultSyncManager, + dispatcherManager = dispatcherManager, + ) + @Provides @Singleton fun provideVaultSyncManager( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index ee813706e6f..7590d9f8bb3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -1,28 +1,22 @@ package com.x8bit.bitwarden.data.vault.repository -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.SendView import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherType -import com.bitwarden.vault.CipherView -import com.bitwarden.vault.FolderView import com.x8bit.bitwarden.data.vault.manager.CipherManager import com.x8bit.bitwarden.data.vault.manager.FolderManager import com.x8bit.bitwarden.data.vault.manager.SendManager +import com.x8bit.bitwarden.data.vault.manager.VaultDataManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager -import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem 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.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult -import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow import java.time.Instant import javax.crypto.Cipher @@ -34,57 +28,15 @@ interface VaultRepository : CipherManager, FolderManager, SendManager, + VaultDataManager, VaultLockManager, VaultSyncManager { - /** - * The [VaultFilterType] for the current user. - * - * Note that this does not affect the data provided by the repository and can be used by - * the UI for consistent filtering across screens. - */ - var vaultFilterType: VaultFilterType - /** * Flow that represents the totp code. */ val totpCodeFlow: Flow - /** - * Completely remove any persisted data from the vault. - */ - fun deleteVaultData(userId: String) - - /** - * Flow that represents the data for a specific vault item as found by ID. This may emit `null` - * if the item cannot be found. - */ - fun getVaultItemStateFlow(itemId: String): StateFlow> - - /** - * Flow that represents the data for a specific vault folder as found by ID. This may emit - * `null` if the folder cannot be found. - */ - fun getVaultFolderStateFlow(folderId: String): StateFlow> - - /** - * Flow that represents the data for a specific send as found by ID. This may emit `null` if - * the send cannot be found. - */ - fun getSendStateFlow(sendId: String): StateFlow> - - /** - * Flow that represents the data for a single verification code item. - * This may emit null if any issues arise during code generation. - */ - fun getAuthCodeFlow(cipherId: String): StateFlow> - - /** - * Flow that represents the data for the TOTP verification codes for ciphers items. - * This may emit an empty list if any issues arise during code generation. - */ - fun getAuthCodesFlow(): StateFlow>> - /** * Silently discovers FIDO 2 credentials for a given [userId] and [relyingPartyId]. */ @@ -169,10 +121,4 @@ interface VaultRepository : * @param ciphers Ciphers selected for export. */ suspend fun exportVaultDataToCxf(ciphers: List): Result - - /** - * Flow that represents the data for a specific vault list item as found by ID. This may emit - * `null` if the item cannot be found. - */ - fun getVaultListItemStateFlow(itemId: String): StateFlow> } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 89d52c35481..e6c11289b2e 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -1,24 +1,16 @@ package com.x8bit.bitwarden.data.vault.repository import com.bitwarden.core.InitUserCryptoMethod -import com.bitwarden.core.data.manager.dispatcher.DispatcherManager import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.repository.error.MissingPropertyException -import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow -import com.bitwarden.core.data.repository.util.combineDataStates -import com.bitwarden.core.data.repository.util.map -import com.bitwarden.core.data.repository.util.mapNullable import com.bitwarden.core.data.util.asFailure import com.bitwarden.exporters.ExportFormat import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.sdk.Fido2CredentialStore -import com.bitwarden.send.SendView import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherListViewType import com.bitwarden.vault.CipherType -import com.bitwarden.vault.CipherView -import com.bitwarden.vault.FolderView import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.autofill.util.login @@ -32,12 +24,10 @@ import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.FolderManager import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManager import com.x8bit.bitwarden.data.vault.manager.SendManager -import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager +import com.x8bit.bitwarden.data.vault.manager.VaultDataManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager -import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult -import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem 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 @@ -48,21 +38,9 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder import com.x8bit.bitwarden.data.vault.repository.util.toSdkAccount import com.x8bit.bitwarden.data.vault.repository.util.toSdkMasterPasswordUnlock -import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType -import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import timber.log.Timber import java.security.GeneralSecurityException import java.time.Instant @@ -80,180 +58,25 @@ internal class VaultRepositoryImpl( private val folderManager: FolderManager, private val sendManager: SendManager, private val vaultLockManager: VaultLockManager, - private val totpCodeManager: TotpCodeManager, private val vaultSyncManager: VaultSyncManager, private val credentialExchangeImportManager: CredentialExchangeImportManager, private val pinProtectedUserKeyManager: PinProtectedUserKeyManager, private val featureFlagManager: FeatureFlagManager, - dispatcherManager: DispatcherManager, + vaultDataManager: VaultDataManager, ) : VaultRepository, CipherManager by cipherManager, FolderManager by folderManager, SendManager by sendManager, + VaultDataManager by vaultDataManager, VaultLockManager by vaultLockManager, VaultSyncManager by vaultSyncManager { - - private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) - private val ioScope = CoroutineScope(dispatcherManager.io) - private val activeUserId: String? get() = authDiskSource.userState?.activeUserId private val mutableTotpCodeResultFlow = bufferedMutableSharedFlow() - override var vaultFilterType: VaultFilterType = VaultFilterType.AllVaults - override val totpCodeFlow: Flow get() = mutableTotpCodeResultFlow.asSharedFlow() - override fun deleteVaultData(userId: String) { - ioScope.launch { - vaultDiskSource.deleteVaultData(userId) - } - } - - override fun getVaultItemStateFlow(itemId: String): StateFlow> = - vaultSyncManager - .vaultDataStateFlow - .map { dataState -> - dataState.map { vaultData -> - val getCipherResult = vaultData - .decryptCipherListResult - .successes - .find { it.id == itemId } - .let { getCipher(itemId) } - when (getCipherResult) { - is GetCipherResult.Success -> getCipherResult.cipherView - else -> null - } - } - } - .stateIn( - scope = unconfinedScope, - started = SharingStarted.Lazily, - initialValue = DataState.Loading, - ) - - override fun getVaultListItemStateFlow(itemId: String): StateFlow> = - vaultSyncManager - .vaultDataStateFlow - .map { dataState -> - dataState.map { vaultData -> - vaultData - .decryptCipherListResult - .successes - .find { it.id == itemId } - } - } - .stateIn( - scope = unconfinedScope, - started = SharingStarted.Lazily, - initialValue = DataState.Loading, - ) - - override fun getVaultFolderStateFlow(folderId: String): StateFlow> = - vaultSyncManager - .vaultDataStateFlow - .map { dataState -> - dataState.map { vaultData -> - vaultData - .folderViewList - .find { it.id == folderId } - } - } - .stateIn( - scope = unconfinedScope, - started = SharingStarted.Lazily, - initialValue = DataState.Loading, - ) - - override fun getSendStateFlow(sendId: String): StateFlow> = - vaultSyncManager - .sendDataStateFlow - .map { dataState -> - dataState.map { sendData -> - sendData.sendViewList.find { it.id == sendId } - } - } - .stateIn( - scope = unconfinedScope, - started = SharingStarted.Lazily, - initialValue = DataState.Loading, - ) - - @OptIn(ExperimentalCoroutinesApi::class) - override fun getAuthCodesFlow(): StateFlow>> { - val userId = activeUserId ?: return MutableStateFlow( - DataState.Error(IllegalStateException("No active user"), null), - ) - return vaultSyncManager - .vaultDataStateFlow - .map { dataState -> - dataState.map { vaultData -> - vaultData - .decryptCipherListResult - .successes - .filter { - it.type is CipherListViewType.Login && - !it.login?.totp.isNullOrBlank() && - it.isActive - } - .toFilteredList(vaultFilterType) - } - } - .flatMapLatest { cipherDataState -> - val cipherList = cipherDataState.data ?: emptyList() - totpCodeManager - .getTotpCodesForCipherListViewsStateFlow( - userId = userId, - cipherListViews = cipherList, - ) - .map { verificationCodeDataStates -> - combineDataStates( - verificationCodeDataStates, - cipherDataState, - ) { verificationCodeItems, _ -> - // Just return the verification items; we are only combining the - // DataStates to know the overall state. - verificationCodeItems - } - } - } - .stateIn( - scope = unconfinedScope, - started = SharingStarted.WhileSubscribed(), - initialValue = DataState.Loading, - ) - } - - @OptIn(ExperimentalCoroutinesApi::class) - override fun getAuthCodeFlow(cipherId: String): StateFlow> { - val userId = activeUserId ?: return MutableStateFlow( - DataState.Error(IllegalStateException("No active user"), null), - ) - return this.getVaultListItemStateFlow(cipherId) - .flatMapLatest { cipherDataState -> - cipherDataState - .data - ?.let { - totpCodeManager - .getTotpCodeStateFlow(userId = userId, cipherListView = it) - .map { totpCodeDataState -> - combineDataStates(totpCodeDataState, cipherDataState) { _, _ -> - // We are only combining the DataStates to know the overall - // state, we map it to the appropriate value below. - } - .mapNullable { totpCodeDataState.data } - } - } - ?: flowOf(DataState.Loaded(null)) - } - .stateIn( - scope = unconfinedScope, - started = SharingStarted.WhileSubscribed(), - initialValue = DataState.Loading, - ) - } - override suspend fun silentlyDiscoverCredentials( userId: String, fido2CredentialStore: Fido2CredentialStore, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt index 464d37cf9e4..6816ac3b4a1 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.data.vault.repository.di -import com.bitwarden.core.data.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource @@ -10,7 +9,7 @@ import com.x8bit.bitwarden.data.vault.manager.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.FolderManager import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManager import com.x8bit.bitwarden.data.vault.manager.SendManager -import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager +import com.x8bit.bitwarden.data.vault.manager.VaultDataManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager import com.x8bit.bitwarden.data.vault.repository.VaultRepository @@ -38,11 +37,10 @@ object VaultRepositoryModule { folderManager: FolderManager, sendManager: SendManager, vaultLockManager: VaultLockManager, - dispatcherManager: DispatcherManager, - totpCodeManager: TotpCodeManager, vaultSyncManager: VaultSyncManager, credentialExchangeImportManager: CredentialExchangeImportManager, pinProtectedUserKeyManager: PinProtectedUserKeyManager, + vaultDataManager: VaultDataManager, featureFlagManager: FeatureFlagManager, ): VaultRepository = VaultRepositoryImpl( vaultDiskSource = vaultDiskSource, @@ -52,11 +50,10 @@ object VaultRepositoryModule { folderManager = folderManager, sendManager = sendManager, vaultLockManager = vaultLockManager, - dispatcherManager = dispatcherManager, - totpCodeManager = totpCodeManager, vaultSyncManager = vaultSyncManager, credentialExchangeImportManager = credentialExchangeImportManager, pinProtectedUserKeyManager = pinProtectedUserKeyManager, + vaultDataManager = vaultDataManager, featureFlagManager = featureFlagManager, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/event/OrganizationEventManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/event/OrganizationEventManagerTest.kt index c060512323a..2400d9dd9c0 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/event/OrganizationEventManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/event/OrganizationEventManagerTest.kt @@ -18,7 +18,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.createMockOrganization import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView -import com.x8bit.bitwarden.data.vault.repository.VaultRepository +import com.x8bit.bitwarden.data.vault.manager.VaultDataManager import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -46,7 +46,7 @@ class OrganizationEventManagerTest { private val mutableVaultItemStateFlow = MutableStateFlow>( value = DataState.Loading, ) - private val vaultRepository = mockk { + private val vaultDataManager = mockk { every { getVaultItemStateFlow(itemId = any()) } returns mutableVaultItemStateFlow } private val eventService = mockk() @@ -73,7 +73,7 @@ class OrganizationEventManagerTest { authDiskSource = fakeAuthDiskSource, authStateManager = authStateManager, organizationManager = organizationManager, - vaultRepository = vaultRepository, + vaultDataManager = vaultDataManager, eventService = eventService, eventDiskSource = eventDiskSource, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultDataManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultDataManagerTest.kt new file mode 100644 index 00000000000..00073c7f140 --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultDataManagerTest.kt @@ -0,0 +1,237 @@ +package com.x8bit.bitwarden.data.vault.manager + +import app.cash.turbine.test +import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager +import com.bitwarden.core.data.repository.model.DataState +import com.bitwarden.send.SendView +import com.bitwarden.vault.CipherView +import com.bitwarden.vault.DecryptCipherListResult +import com.bitwarden.vault.FolderView +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.vault.datasource.disk.VaultDiskSource +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView +import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem +import com.x8bit.bitwarden.data.vault.repository.model.SendData +import com.x8bit.bitwarden.data.vault.repository.model.VaultData +import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem +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 kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class VaultDataManagerTest { + + private val fakeAuthDiskSource = FakeAuthDiskSource() + private val vaultDiskSource: VaultDiskSource = mockk() + private val cipherManager: CipherManager = mockk() + private val totpCodeManager: TotpCodeManager = mockk() + private val mutableVaultDataStateFlow = + MutableStateFlow>(DataState.Loading) + private val mutableSendDataStateFlow = MutableStateFlow>(DataState.Loading) + private val vaultSyncManager: VaultSyncManager = mockk { + every { vaultDataStateFlow } returns mutableVaultDataStateFlow + every { sendDataStateFlow } returns mutableSendDataStateFlow + } + + private val vaultDataManager: VaultDataManager = VaultDataManagerImpl( + authDiskSource = fakeAuthDiskSource, + cipherManager = cipherManager, + totpCodeManager = totpCodeManager, + vaultDiskSource = vaultDiskSource, + vaultSyncManager = vaultSyncManager, + dispatcherManager = FakeDispatcherManager(), + ) + + @Test + fun `getAuthCodeFlow with no active user should emit an error`() = runTest { + fakeAuthDiskSource.userState = null + assertTrue(vaultDataManager.getAuthCodeFlow(cipherId = "cipherId").value is DataState.Error) + } + + @Test + fun `getAuthCodeFlow for a single cipher should update data state when state changes`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val cipherId = "mockId-1" + val stateFlow = MutableStateFlow>(DataState.Loading) + + every { + totpCodeManager.getTotpCodeStateFlow(userId = userId, cipherListView = any()) + } returns stateFlow + mutableVaultDataStateFlow.value = DataState.Loaded( + data = VaultData( + decryptCipherListResult = DecryptCipherListResult( + successes = listOf(createMockCipherListView(number = 1)), + failures = emptyList(), + ), + collectionViewList = emptyList(), + folderViewList = emptyList(), + sendViewList = emptyList(), + ), + ) + + vaultDataManager.getAuthCodeFlow(cipherId = cipherId).test { + assertEquals(DataState.Loading, awaitItem()) + + stateFlow.tryEmit(DataState.Loaded(data = createVerificationCodeItem())) + assertEquals(DataState.Loaded(data = createVerificationCodeItem()), awaitItem()) + } + } + + @Test + fun `getAuthCodesFlow with no active user should emit an error`() = runTest { + fakeAuthDiskSource.userState = null + assertTrue(vaultDataManager.getAuthCodesFlow().value is DataState.Error) + } + + @Test + fun `getAuthCodesFlow should update data state when state changes`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val userId = "mockId-1" + val stateFlow = MutableStateFlow>>(DataState.Loading) + + every { + totpCodeManager.getTotpCodesForCipherListViewsStateFlow( + userId = userId, + cipherListViews = any(), + ) + } returns stateFlow + mutableVaultDataStateFlow.value = DataState.Loaded( + data = VaultData( + decryptCipherListResult = DecryptCipherListResult( + successes = listOf(createMockCipherListView(number = 1)), + failures = emptyList(), + ), + collectionViewList = emptyList(), + folderViewList = emptyList(), + sendViewList = emptyList(), + ), + ) + + vaultDataManager.getAuthCodesFlow().test { + assertEquals(DataState.Loading, awaitItem()) + + stateFlow.tryEmit(DataState.Loaded(data = listOf(createVerificationCodeItem()))) + assertEquals(DataState.Loaded(data = listOf(createVerificationCodeItem())), awaitItem()) + } + } + + @Test + fun `deleteVaultData should call deleteVaultData on VaultDiskSource`() { + val userId = "userId-1234" + coEvery { vaultDiskSource.deleteVaultData(userId = userId) } just runs + + vaultDataManager.deleteVaultData(userId = userId) + + coVerify(exactly = 1) { + vaultDiskSource.deleteVaultData(userId = userId) + } + } + + @Test + fun `getVaultItemStateFlow should update to Error when error state is emitted`() = + runTest { + val folderId = 1234 + val folderIdString = "mockId-$folderId" + val throwable = Throwable("Fail") + + vaultDataManager.getVaultItemStateFlow(itemId = folderIdString).test { + assertEquals(DataState.Loading, awaitItem()) + mutableVaultDataStateFlow.value = DataState.Error(error = throwable) + assertEquals(DataState.Error(error = throwable), awaitItem()) + } + } + + @Test + fun `getVaultItemStateFlow should update to NoNetwork when a NoNetwork value is emitted`() = + runTest { + val itemId = 1234 + val itemIdString = "mockId-$itemId" + + vaultDataManager.getVaultItemStateFlow(itemId = itemIdString).test { + assertEquals(DataState.Loading, awaitItem()) + mutableVaultDataStateFlow.value = DataState.NoNetwork() + assertEquals(DataState.NoNetwork(), awaitItem()) + } + } + + @Test + fun `getVaultFolderStateFlow should update to NoNetwork when no network value is emitted`() = + runTest { + val folderId = 1234 + val folderIdString = "mockId-$folderId" + + vaultDataManager.getVaultFolderStateFlow(folderId = folderIdString).test { + assertEquals(DataState.Loading, awaitItem()) + mutableVaultDataStateFlow.value = DataState.NoNetwork() + assertEquals(DataState.NoNetwork(), awaitItem()) + } + } + + @Test + fun `getVaultFolderStateFlow should update to Error when an error is emitted`() = + runTest { + val folderId = 1234 + val folderIdString = "mockId-$folderId" + val throwable = Throwable("Fail") + + vaultDataManager.getVaultFolderStateFlow(folderId = folderIdString).test { + assertEquals(DataState.Loading, awaitItem()) + mutableVaultDataStateFlow.value = DataState.Error(error = throwable) + assertEquals(DataState.Error(error = throwable), awaitItem()) + } + } + + @Test + fun `getSendStateFlow should update emit SendView when present`() = runTest { + val sendId = 1 + val sendView = createMockSendView(number = sendId) + + vaultDataManager.getSendStateFlow(sendId = "mockId-$sendId").test { + assertEquals(DataState.Loading, awaitItem()) + mutableSendDataStateFlow.value = DataState.Loaded(data = SendData(emptyList())) + assertEquals(DataState.Loaded(data = null), awaitItem()) + mutableSendDataStateFlow.value = DataState.Loaded(SendData(listOf(sendView))) + assertEquals(DataState.Loaded(data = sendView), awaitItem()) + } + } + + @Test + fun `getSendStateFlow should update to NoNetwork when NoNetwork value is emitted`() = + runTest { + val sendId = 1234 + vaultDataManager.getSendStateFlow(sendId = "mockId-$sendId").test { + assertEquals(DataState.Loading, awaitItem()) + mutableSendDataStateFlow.value = DataState.NoNetwork() + assertEquals(DataState.NoNetwork(), awaitItem()) + } + } + + @Test + fun `getSendStateFlow should update to Error when an error is emitted`() = + runTest { + val sendId = 1234 + val throwable = Throwable("Fail") + + vaultDataManager.getSendStateFlow(sendId = "mockId-$sendId").test { + assertEquals(DataState.Loading, awaitItem()) + mutableSendDataStateFlow.value = DataState.Error(error = throwable) + assertEquals(DataState.Error(error = throwable), awaitItem()) + } + } +} + +private val MOCK_USER_STATE: UserStateJson = UserStateJson( + activeUserId = "mockId-1", + accounts = mapOf("mockId-1" to mockk()), +) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 023259fdef5..5d8d05fb3b8 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -1,11 +1,8 @@ package com.x8bit.bitwarden.data.vault.repository -import app.cash.turbine.test import com.bitwarden.collections.CollectionView import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.MasterPasswordUnlockData -import com.bitwarden.core.data.manager.dispatcher.DispatcherManager -import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.repository.error.MissingPropertyException import com.bitwarden.core.data.repository.model.DataState @@ -21,10 +18,8 @@ import com.bitwarden.network.model.createMockCipher import com.bitwarden.network.model.createMockFolder import com.bitwarden.network.model.createMockOrganizationKeys import com.bitwarden.sdk.Fido2CredentialStore -import com.bitwarden.send.SendView import com.bitwarden.vault.CipherListViewType import com.bitwarden.vault.CipherType -import com.bitwarden.vault.CipherView import com.bitwarden.vault.DecryptCipherListResult import com.bitwarden.vault.FolderView import com.bitwarden.vault.TotpResponse @@ -48,12 +43,10 @@ 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.CredentialExchangeImportManager import com.x8bit.bitwarden.data.vault.manager.PinProtectedUserKeyManager -import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager import com.x8bit.bitwarden.data.vault.manager.VaultLockManager import com.x8bit.bitwarden.data.vault.manager.VaultSyncManager 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.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult @@ -63,7 +56,6 @@ 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.toEncryptedSdkCipher import com.x8bit.bitwarden.data.vault.repository.util.toSdkMasterPasswordUnlock -import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -87,14 +79,12 @@ import javax.crypto.Cipher @Suppress("LargeClass") class VaultRepositoryTest { - private val dispatcherManager: DispatcherManager = FakeDispatcherManager() private val fakeAuthDiskSource = FakeAuthDiskSource() private val mutableGetCiphersFlow: MutableStateFlow> = MutableStateFlow(listOf(createMockCipher(1))) private val vaultDiskSource: VaultDiskSource = mockk { every { getCiphersFlow(any()) } returns mutableGetCiphersFlow } - private val totpCodeManager: TotpCodeManager = mockk() private val vaultSdkSource: VaultSdkSource = mockk() private val vaultLockManager: VaultLockManager = mockk() private val mutableVaultDataStateFlow = @@ -129,8 +119,6 @@ class VaultRepositoryTest { vaultSdkSource = vaultSdkSource, authDiskSource = fakeAuthDiskSource, vaultLockManager = vaultLockManager, - dispatcherManager = dispatcherManager, - totpCodeManager = totpCodeManager, cipherManager = mockk(), folderManager = mockk(), sendManager = mockk(), @@ -138,6 +126,7 @@ class VaultRepositoryTest { credentialExchangeImportManager = credentialExchangeImportManager, pinProtectedUserKeyManager = pinProtectedUserKeyManager, featureFlagManager = featureFlagManager, + vaultDataManager = mockk(), ) @BeforeEach @@ -158,18 +147,6 @@ class VaultRepositoryTest { unmockkConstructor(MissingPropertyException::class) } - @Test - fun `deleteVaultData should call deleteVaultData on VaultDiskSource`() { - val userId = "userId-1234" - coEvery { vaultDiskSource.deleteVaultData(userId) } just runs - - vaultRepository.deleteVaultData(userId = userId) - - coVerify(exactly = 1) { - vaultDiskSource.deleteVaultData(userId) - } - } - @Test fun `unlockVaultWithBiometrics with missing user state should return InvalidStateError`() = runTest { @@ -955,106 +932,6 @@ class VaultRepositoryTest { } } - @Test - fun `getVaultItemStateFlow should update to Error when error state is emitted`() = - runTest { - val folderId = 1234 - val folderIdString = "mockId-$folderId" - val throwable = Throwable("Fail") - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.getVaultItemStateFlow(folderIdString).test { - assertEquals(DataState.Loading, awaitItem()) - mutableVaultDataStateFlow.value = DataState.Error(throwable) - assertEquals(DataState.Error(throwable), awaitItem()) - } - } - - @Test - fun `getVaultItemStateFlow should update to NoNetwork when a NoNetwork value is emitted`() = - runTest { - val itemId = 1234 - val itemIdString = "mockId-$itemId" - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.getVaultItemStateFlow(itemIdString).test { - assertEquals(DataState.Loading, awaitItem()) - mutableVaultDataStateFlow.value = DataState.NoNetwork() - assertEquals(DataState.NoNetwork(), awaitItem()) - } - } - - @Test - fun `getVaultFolderStateFlow should update to NoNetwork when no network value is emitted`() = - runTest { - val folderId = 1234 - val folderIdString = "mockId-$folderId" - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.getVaultFolderStateFlow(folderIdString).test { - assertEquals(DataState.Loading, awaitItem()) - mutableVaultDataStateFlow.value = DataState.NoNetwork() - assertEquals(DataState.NoNetwork(), awaitItem()) - } - } - - @Test - fun `getVaultFolderStateFlow should update to Error when an error is emitted`() = - runTest { - val folderId = 1234 - val folderIdString = "mockId-$folderId" - val throwable = Throwable("Fail") - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.getVaultFolderStateFlow(folderIdString).test { - assertEquals(DataState.Loading, awaitItem()) - mutableVaultDataStateFlow.value = DataState.Error(throwable) - assertEquals(DataState.Error(throwable), awaitItem()) - } - } - - @Test - fun `getSendStateFlow should update emit SendView when present`() = runTest { - val sendId = 1 - fakeAuthDiskSource.userState = MOCK_USER_STATE - val sendView = createMockSendView(number = sendId) - - vaultRepository.getSendStateFlow("mockId-$sendId").test { - assertEquals(DataState.Loading, awaitItem()) - mutableSendDataStateFlow.value = DataState.Loaded(SendData(emptyList())) - assertEquals(DataState.Loaded(null), awaitItem()) - mutableSendDataStateFlow.value = DataState.Loaded(SendData(listOf(sendView))) - assertEquals(DataState.Loaded(sendView), awaitItem()) - } - } - - @Test - fun `getSendStateFlow should update to NoNetwork when NoNetwork value is emitted`() = - runTest { - val sendId = 1234 - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.getSendStateFlow("mockId-$sendId").test { - assertEquals(DataState.Loading, awaitItem()) - mutableSendDataStateFlow.value = DataState.NoNetwork() - assertEquals(DataState.NoNetwork(), awaitItem()) - } - } - - @Test - fun `getSendStateFlow should update to Error when an error is emitted`() = - runTest { - val sendId = 1234 - val throwable = Throwable("Fail") - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.getSendStateFlow("mockId-$sendId").test { - assertEquals(DataState.Loading, awaitItem()) - mutableSendDataStateFlow.value = DataState.Error(throwable) - assertEquals(DataState.Error(throwable), awaitItem()) - } - } - @Test fun `generateTotp with no active user should return GenerateTotpResult Error`() = runTest { @@ -1299,94 +1176,6 @@ class VaultRepositoryTest { assertEquals(setOf("mockId-1"), result) } - @Test - fun `getAuthCodeFlow with no active user should emit an error`() = runTest { - fakeAuthDiskSource.userState = null - assertTrue(vaultRepository.getAuthCodeFlow(cipherId = "cipherId").value is DataState.Error) - } - - @Test - fun `getAuthCodeFlow for a single cipher should update data state when state changes`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val stateFlow = MutableStateFlow>(DataState.Loading) - - every { - totpCodeManager.getTotpCodeStateFlow(userId = userId, cipherListView = any()) - } returns stateFlow - mutableVaultDataStateFlow.value = DataState.Loaded( - data = VaultData( - decryptCipherListResult = DecryptCipherListResult( - successes = listOf(createMockCipherListView(number = 1)), - failures = emptyList(), - ), - collectionViewList = emptyList(), - folderViewList = emptyList(), - sendViewList = emptyList(), - ), - ) - - vaultRepository.getAuthCodeFlow(userId).test { - assertEquals( - DataState.Loading, - awaitItem(), - ) - - stateFlow.tryEmit(DataState.Loaded(createVerificationCodeItem())) - - assertEquals( - DataState.Loaded(createVerificationCodeItem()), - awaitItem(), - ) - } - } - - @Test - fun `getAuthCodesFlow with no active user should emit an error`() = runTest { - fakeAuthDiskSource.userState = null - assertTrue(vaultRepository.getAuthCodesFlow().value is DataState.Error) - } - - @Test - fun `getAuthCodesFlow should update data state when state changes`() = runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val userId = "mockId-1" - val stateFlow = MutableStateFlow>>(DataState.Loading) - - every { - totpCodeManager.getTotpCodesForCipherListViewsStateFlow( - userId = userId, - cipherListViews = any(), - ) - } returns stateFlow - mutableVaultDataStateFlow.value = DataState.Loaded( - data = VaultData( - decryptCipherListResult = DecryptCipherListResult( - successes = listOf(createMockCipherListView(number = 1)), - failures = emptyList(), - ), - collectionViewList = emptyList(), - folderViewList = emptyList(), - sendViewList = emptyList(), - ), - ) - - vaultRepository.getAuthCodesFlow().test { - assertEquals( - DataState.Loading, - awaitItem(), - ) - - stateFlow.tryEmit(DataState.Loaded(listOf(createVerificationCodeItem()))) - - assertEquals( - DataState.Loaded(listOf(createVerificationCodeItem())), - awaitItem(), - ) - } - } - @Suppress("MaxLineLength") @Test fun `exportVaultDataToString should return a success result when data is successfully converted for export`() =