From feb37ccaf33f556034d255d2080f9830b1337c72 Mon Sep 17 00:00:00 2001 From: David Perez Date: Thu, 21 Dec 2023 08:58:22 -0600 Subject: [PATCH] VaultData should come directly from the database (#425) --- .../repository/util/DataStateExtensions.kt | 99 ++ .../vault/repository/VaultRepositoryImpl.kt | 122 +-- .../util/DataStateExtensionsTest.kt | 207 +++++ .../vault/repository/VaultRepositoryTest.kt | 860 ++++-------------- 4 files changed, 503 insertions(+), 785 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/DataStateExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/DataStateExtensions.kt index fe9071b365..db11d90d44 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/DataStateExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/util/DataStateExtensions.kt @@ -38,3 +38,102 @@ fun MutableStateFlow>.updateToPendingOrLoading() { ?: DataState.Loading } } + +/** + * Combines the [dataState1] and [dataState2] [DataState]s together using the provided [transform]. + * + * This function will internally manage the final `DataState` type that is returned. This is done + * by prioritizing each if the states in this order: + * + * - [DataState.Error] + * - [DataState.NoNetwork] + * - [DataState.Loading] + * - [DataState.Pending] + * - [DataState.Loaded] + * + * This priority order ensures that the total state is accurately reflecting the underlying states. + * If one of the `DataState`s has a higher priority than the other, the output will be the highest + * priority. For example, if one `DataState` is `DataState.Loaded` and another is `DataState.Error` + * then the returned `DataState` will be `DataState.Error`. + * + * The payload of the final `DataState` be created by the `transform` lambda which will be invoked + * whenever the payloads of both `dataState1` and `dataState2` are not null. In the scenario where + * one or both payloads are null, the resulting `DataState` will have a null payload. + */ +fun combineDataStates( + dataState1: DataState, + dataState2: DataState, + transform: (t1: T1, t2: T2) -> R, +): DataState { + // Wraps the `transform` lambda to allow null data to be passed in. If either of the passed in + // values are null, the regular transform will not be invoked and null is returned. + val nullableTransform: (T1?, T2?) -> R? = { t1, t2 -> + if (t1 != null && t2 != null) transform(t1, t2) else null + } + return when { + // Error states have highest priority, fail fast. + dataState1 is DataState.Error -> { + DataState.Error( + error = dataState1.error, + data = nullableTransform(dataState1.data, dataState2.data), + ) + } + + dataState2 is DataState.Error -> { + DataState.Error( + error = dataState2.error, + data = nullableTransform(dataState1.data, dataState2.data), + ) + } + + dataState1 is DataState.NoNetwork || dataState2 is DataState.NoNetwork -> { + DataState.NoNetwork(nullableTransform(dataState1.data, dataState2.data)) + } + + // Something is still loading, we will wait for all the data. + dataState1 is DataState.Loading || dataState2 is DataState.Loading -> DataState.Loading + + // Pending state for everything while any one piece of data is updating. + dataState1 is DataState.Pending || dataState2 is DataState.Pending -> { + DataState.Pending( + transform(requireNotNull(dataState1.data), requireNotNull(dataState2.data)), + ) + } + + // Both states are Loaded and have data + else -> { + DataState.Loaded( + transform(requireNotNull(dataState1.data), requireNotNull(dataState2.data)), + ) + } + } +} + +/** + * Combines the [dataState1], [dataState2], and [dataState3] [DataState]s together using the + * provided [transform]. + * + * See [combineDataStates] for details. + */ +fun combineDataStates( + dataState1: DataState, + dataState2: DataState, + dataState3: DataState, + transform: (t1: T1, t2: T2, t3: T3) -> R, +): DataState = + dataState1 + .combineDataStatesWith(dataState2) { t1, t2 -> t1 to t2 } + .combineDataStatesWith(dataState3) { t1t2Pair, t3 -> + transform(t1t2Pair.first, t1t2Pair.second, t3) + } + +/** + * Combines [dataState2] with the given [DataState] using the provided [transform]. + * + * See [combineDataStates] for details. + */ +fun DataState.combineDataStatesWith( + dataState2: DataState, + transform: (t1: T1, t2: T2) -> R, +): DataState = + combineDataStates(this, dataState2, transform) 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 eba4084b42..52f7ff42f8 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 @@ -13,12 +13,12 @@ import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.repository.model.DataState +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.platform.util.zip import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService @@ -39,12 +39,12 @@ 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.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow 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 @@ -55,7 +55,12 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext + +/** + * A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the + * specified period of time after it no longer has subscribers. + */ +private const val STOP_TIMEOUT_DELAY_MS: Long = 1000L /** * Default implementation of [VaultRepository]. @@ -67,10 +72,11 @@ class VaultRepositoryImpl( private val vaultDiskSource: VaultDiskSource, private val vaultSdkSource: VaultSdkSource, private val authDiskSource: AuthDiskSource, - private val dispatcherManager: DispatcherManager, + dispatcherManager: DispatcherManager, ) : VaultRepository { - private val scope = CoroutineScope(dispatcherManager.io) + private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined) + private val ioScope = CoroutineScope(dispatcherManager.io) private var syncJob: Job = Job().apply { complete() } @@ -78,9 +84,6 @@ class VaultRepositoryImpl( private val activeUserId: String? get() = authDiskSource.userState?.activeUserId - private val mutableVaultDataStateFlow = - MutableStateFlow>(DataState.Loading) - private val mutableVaultStateStateFlow = MutableStateFlow(VaultState(unlockedVaultUserIds = emptySet())) @@ -95,8 +98,29 @@ class VaultRepositoryImpl( private val mutableCollectionsStateFlow = MutableStateFlow>>(DataState.Loading) - override val vaultDataStateFlow: StateFlow> - get() = mutableVaultDataStateFlow.asStateFlow() + override val vaultDataStateFlow: StateFlow> = + combine( + ciphersStateFlow, + foldersStateFlow, + collectionsStateFlow, + ) { ciphersDataState, foldersDataState, collectionsDataState -> + combineDataStates( + ciphersDataState, + foldersDataState, + collectionsDataState, + ) { ciphersData, foldersData, collectionsData -> + VaultData( + cipherViewList = ciphersData, + folderViewList = foldersData, + collectionViewList = collectionsData, + ) + } + } + .stateIn( + scope = unconfinedScope, + started = SharingStarted.WhileSubscribed(stopTimeoutMillis = STOP_TIMEOUT_DELAY_MS), + initialValue = DataState.Loading, + ) override val ciphersStateFlow: StateFlow>> get() = mutableCiphersStateFlow.asStateFlow() @@ -119,31 +143,30 @@ class VaultRepositoryImpl( .observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId -> observeVaultDiskCiphers(activeUserId) } - .launchIn(scope) + .launchIn(unconfinedScope) // Setup folders MutableStateFlow mutableFoldersStateFlow .observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId -> observeVaultDiskFolders(activeUserId) } - .launchIn(scope) + .launchIn(unconfinedScope) // Setup collections MutableStateFlow mutableCollectionsStateFlow .observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId -> observeVaultDiskCollections(activeUserId) } - .launchIn(scope) + .launchIn(unconfinedScope) } override fun clearUnlockedData() { mutableCiphersStateFlow.update { DataState.Loading } mutableFoldersStateFlow.update { DataState.Loading } mutableCollectionsStateFlow.update { DataState.Loading } - mutableVaultDataStateFlow.update { DataState.Loading } mutableSendDataStateFlow.update { DataState.Loading } } override fun deleteVaultData(userId: String) { - scope.launch { + ioScope.launch { vaultDiskSource.deleteVaultData(userId) } } @@ -154,9 +177,8 @@ class VaultRepositoryImpl( mutableCiphersStateFlow.updateToPendingOrLoading() mutableFoldersStateFlow.updateToPendingOrLoading() mutableCollectionsStateFlow.updateToPendingOrLoading() - mutableVaultDataStateFlow.updateToPendingOrLoading() mutableSendDataStateFlow.updateToPendingOrLoading() - syncJob = scope.launch { + syncJob = ioScope.launch { syncService .sync() .fold( @@ -170,10 +192,7 @@ class VaultRepositoryImpl( unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse) storeKeys(syncResponse = syncResponse) - decryptSyncResponseAndUpdateVaultDataState( - userId = userId, - syncResponse = syncResponse, - ) + vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse) decryptSendsAndUpdateSendDataState(sendList = syncResponse.sends) }, onFailure = { throwable -> @@ -192,11 +211,6 @@ class VaultRepositoryImpl( data = currentState.data, ) } - mutableVaultDataStateFlow.update { currentState -> - throwable.toNetworkOrErrorState( - data = currentState.data, - ) - } mutableSendDataStateFlow.update { currentState -> throwable.toNetworkOrErrorState( data = currentState.data, @@ -217,7 +231,7 @@ class VaultRepositoryImpl( } } .stateIn( - scope = scope, + scope = unconfinedScope, started = SharingStarted.Lazily, initialValue = DataState.Loading, ) @@ -232,7 +246,7 @@ class VaultRepositoryImpl( } } .stateIn( - scope = scope, + scope = unconfinedScope, started = SharingStarted.Lazily, initialValue = DataState.Loading, ) @@ -450,58 +464,6 @@ class VaultRepositoryImpl( mutableSendDataStateFlow.update { newState } } - private suspend fun decryptSyncResponseAndUpdateVaultDataState( - userId: String, - syncResponse: SyncResponseJson, - ) = withContext(dispatcherManager.default) { - val deferred = async { - vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse) - } - - // Allow decryption of various types in parallel. - val newState = zip( - { - vaultSdkSource - .decryptCipherList( - cipherList = syncResponse - .ciphers - .orEmpty() - .toEncryptedSdkCipherList(), - ) - }, - { - vaultSdkSource - .decryptFolderList( - folderList = syncResponse - .folders - .orEmpty() - .toEncryptedSdkFolderList(), - ) - }, - { - vaultSdkSource - .decryptCollectionList( - collectionList = syncResponse - .collections - .orEmpty() - .toEncryptedSdkCollectionList(), - ) - }, - ) { decryptedCipherList, decryptedFolderList, decryptedCollectionList -> - VaultData( - cipherViewList = decryptedCipherList, - collectionViewList = decryptedCollectionList, - folderViewList = decryptedFolderList, - ) - } - .fold( - onSuccess = { DataState.Loaded(data = it) }, - onFailure = { DataState.Error(error = it) }, - ) - mutableVaultDataStateFlow.update { newState } - deferred.await() - } - private fun observeVaultDiskCiphers( userId: String, ): Flow>> = diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/DataStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/DataStateExtensionsTest.kt index 39624ea025..029841a32a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/DataStateExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/util/DataStateExtensionsTest.kt @@ -41,4 +41,211 @@ class DataStateExtensionsTest { assertEquals(DataState.Loading, mutableStateFlow.value) } + + @Suppress("MaxLineLength") + @Test + fun `combineDataStates should return an empty Error when the first dataState is Error without data`() { + val throwable = Throwable("Fail") + val dataState1 = DataState.Error(throwable) + val dataState2 = DataState.Loaded(5) + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.Error>(throwable), result) + } + + @Suppress("MaxLineLength") + @Test + fun `combineDataStates should return a populated Error when the first dataState is Error with data`() { + val throwable = Throwable("Fail") + val dataState1 = DataState.Error(throwable, "data") + val dataState2 = DataState.Loaded(5) + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.Error(throwable, "data" to 5), result) + } + + @Suppress("MaxLineLength") + @Test + fun `combineDataStates should return an empty Error when the second dataState is Error without data`() { + val throwable = Throwable("Fail") + val dataState1 = DataState.Loaded(5) + val dataState2 = DataState.Error(throwable) + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.Error>(throwable), result) + } + + @Suppress("MaxLineLength") + @Test + fun `combineDataStates should return a populated Error when the second dataState is Error with data`() { + val throwable = Throwable("Fail") + val dataState1 = DataState.Loaded(5) + val dataState2 = DataState.Error(throwable, "data") + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.Error(throwable, 5 to "data"), result) + } + + @Suppress("MaxLineLength") + @Test + fun `combineDataStates should return an empty NoNetwork when the first dataState is NoNetwork without data`() { + val dataState1 = DataState.NoNetwork() + val dataState2 = DataState.Loaded("data") + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.NoNetwork>(), result) + } + + @Suppress("MaxLineLength") + @Test + fun `combineDataStates should return a populated NoNetwork when the first dataState is NoNetwork with data`() { + val dataState1 = DataState.NoNetwork(5) + val dataState2 = DataState.Loaded("data") + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.NoNetwork(5 to "data"), result) + } + + @Suppress("MaxLineLength") + @Test + fun `combineDataStates should return an empty NoNetwork when the second dataState is NoNetwork without data`() { + val dataState1 = DataState.Loaded("data") + val dataState2 = DataState.NoNetwork() + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.NoNetwork>(), result) + } + + @Suppress("MaxLineLength") + @Test + fun `combineDataStates should return a populated NoNetwork when the second dataState is NoNetwork with data`() { + val dataState1 = DataState.Loaded("data") + val dataState2 = DataState.NoNetwork(5) + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.NoNetwork("data" to 5), result) + } + + @Test + fun `combineDataStates should return Loading when the first dataState is Loading`() { + val dataState1: DataState = DataState.Loading + val dataState2 = DataState.Loaded("data") + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.Loading, result) + } + + @Test + fun `combineDataStates should return Loading when the second dataState is Loading`() { + val dataState1 = DataState.Loaded("data") + val dataState2: DataState = DataState.Loading + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.Loading, result) + } + + @Test + fun `combineDataStates should return Pending when the first dataState is Pending`() { + val dataState1 = DataState.Pending(5) + val dataState2 = DataState.Loaded("data") + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.Pending(5 to "data"), result) + } + + @Test + fun `combineDataStates should return Pending when the second dataState is Pending`() { + val dataState1 = DataState.Loaded("data") + val dataState2 = DataState.Pending(5) + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.Pending("data" to 5), result) + } + + @Test + fun `combineDataStates should return Loaded when the both dataStates are Loaded`() { + val dataState1 = DataState.Loaded("data") + val dataState2 = DataState.Loaded(5) + + val result = combineDataStates( + dataState1 = dataState1, + dataState2 = dataState2, + ) { data1, data2 -> + data1 to data2 + } + + assertEquals(DataState.Loaded("data" to 5), result) + } } 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 ad647a5417..eaa23540d6 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 @@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITER import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.repository.model.DataState +import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource @@ -57,6 +58,7 @@ import io.mockk.runs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest @@ -257,7 +259,7 @@ class VaultRepositoryTest { @Suppress("MaxLineLength") @Test - fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and DataStateFlows`() = + fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and sendDataStateFlows`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val mockSyncResponse = createMockSyncResponse(number = 1) @@ -275,15 +277,6 @@ class VaultRepositoryTest { vault = mockSyncResponse, ) } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() coEvery { vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) } returns listOf(createMockSendView(number = 1)).asSuccess() @@ -317,16 +310,6 @@ class VaultRepositoryTest { userId = "mockId-1", organizationKeys = mapOf("mockId-1" to "mockKey-1"), ) - assertEquals( - DataState.Loaded( - data = VaultData( - cipherViewList = listOf(createMockCipherView(number = 1)), - collectionViewList = listOf(createMockCollectionView(number = 1)), - folderViewList = listOf(createMockFolderView(number = 1)), - ), - ), - vaultRepository.vaultDataStateFlow.value, - ) assertEquals( DataState.Loaded( data = SendData( @@ -336,82 +319,14 @@ class VaultRepositoryTest { vaultRepository.sendDataStateFlow.value, ) coVerify { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } - } - - @Test - fun `sync with data should update vaultDataStateFlow to Pending before service sync`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val mockSyncResponse = createMockSyncResponse(number = 1) - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { vaultDiskSource.replaceVaultData( userId = MOCK_USER_STATE.activeUserId, vault = mockSyncResponse, ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() - - vaultRepository.vaultDataStateFlow.test { - assertEquals( - DataState.Loading, - awaitItem(), - ) - vaultRepository.sync() - assertEquals( - DataState.Loaded( - data = VaultData( - cipherViewList = listOf(createMockCipherView(number = 1)), - collectionViewList = listOf(createMockCollectionView(number = 1)), - folderViewList = listOf(createMockFolderView(number = 1)), - ), + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), ), - awaitItem(), - ) - vaultRepository.sync() - assertEquals( - DataState.Pending( - data = VaultData( - cipherViewList = listOf(createMockCipherView(number = 1)), - collectionViewList = listOf(createMockCollectionView(number = 1)), - folderViewList = listOf(createMockFolderView(number = 1)), - ), - ), - awaitItem(), - ) - assertEquals( - DataState.Loaded( - data = VaultData( - cipherViewList = listOf(createMockCipherView(number = 1)), - collectionViewList = listOf(createMockCollectionView(number = 1)), - folderViewList = listOf(createMockFolderView(number = 1)), - ), - ), - awaitItem(), ) } } @@ -434,15 +349,6 @@ class VaultRepositoryTest { vault = mockSyncResponse, ) } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() coEvery { vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) } returns listOf(createMockSendView(number = 1)).asSuccess() @@ -483,91 +389,7 @@ class VaultRepositoryTest { } @Test - fun `sync with decryptCipherList Failure should update vaultDataStateFlow with Error`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val mockException = IllegalStateException() - val mockSyncResponse = createMockSyncResponse(number = 1) - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultDiskSource.replaceVaultData( - userId = MOCK_USER_STATE.activeUserId, - vault = mockSyncResponse, - ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns mockException.asFailure() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.sync() - - assertEquals( - DataState.Error(error = mockException), - vaultRepository.vaultDataStateFlow.value, - ) - } - - @Test - fun `sync with decryptFolderList Failure should update vaultDataStateFlow with Error`() = - runTest { - val mockException = IllegalStateException() - val mockSyncResponse = createMockSyncResponse(number = 1) - fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultDiskSource.replaceVaultData( - userId = MOCK_USER_STATE.activeUserId, - vault = mockSyncResponse, - ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns mockException.asFailure() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.sync() - - assertEquals( - DataState.Error(error = mockException), - vaultRepository.vaultDataStateFlow.value, - ) - } - - @Test - fun `sync with decryptCollectionList Failure should update vaultDataStateFlow with Error`() = + fun `sync with decryptSendList Failure should update sendDataStateFlows with Error`() = runTest { val mockException = IllegalStateException() fakeAuthDiskSource.userState = MOCK_USER_STATE @@ -586,56 +408,6 @@ class VaultRepositoryTest { vault = mockSyncResponse, ) } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns mockException.asFailure() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() - - vaultRepository.sync() - - assertEquals( - DataState.Error(error = mockException), - vaultRepository.vaultDataStateFlow.value, - ) - } - - @Test - fun `sync with decryptSendList Failure should update sendDataStateFlow with Error`() = - runTest { - val mockException = IllegalStateException() - fakeAuthDiskSource.userState = MOCK_USER_STATE - val mockSyncResponse = createMockSyncResponse(number = 1) - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultDiskSource.replaceVaultData( - userId = MOCK_USER_STATE.activeUserId, - vault = mockSyncResponse, - ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() coEvery { vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) } returns mockException.asFailure() @@ -650,129 +422,87 @@ class VaultRepositoryTest { } @Test - fun `sync with syncService Failure should update vault and send DataStateFlow with an Error`() = + fun `sync with syncService Failure should update DataStateFlow with an Error`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - val mockException = IllegalStateException( - "sad", - ) - coEvery { - syncService.sync() - } returns mockException.asFailure() + val mockException = IllegalStateException("sad") + coEvery { syncService.sync() } returns mockException.asFailure() vaultRepository.sync() assertEquals( - DataState.Error( - error = mockException, - data = null, - ), - vaultRepository.vaultDataStateFlow.value, + DataState.Error>(mockException), + vaultRepository.ciphersStateFlow.value, ) assertEquals( - DataState.Error( - error = mockException, - data = null, - ), + DataState.Error>(mockException), + vaultRepository.collectionsStateFlow.value, + ) + assertEquals( + DataState.Error>(mockException), + vaultRepository.foldersStateFlow.value, + ) + assertEquals( + DataState.Error(mockException), vaultRepository.sendDataStateFlow.value, ) } @Test - fun `sync with NoNetwork should update vault and send DataStateFlow to NoNetwork`() = runTest { + fun `sync with syncService Failure should update vaultDataStateFlow with an Error`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val mockException = IllegalStateException("sad") + coEvery { syncService.sync() } returns mockException.asFailure() + setupVaultDiskSourceFlows() + + vaultRepository + .vaultDataStateFlow + .test { + assertEquals(DataState.Loading, awaitItem()) + vaultRepository.sync() + assertEquals(DataState.Error(mockException), awaitItem()) + } + } + + @Test + fun `sync with NoNetwork should update DataStateFlows to NoNetwork`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { syncService.sync() } returns UnknownHostException().asFailure() vaultRepository.sync() assertEquals( - DataState.NoNetwork( - data = null, - ), - vaultRepository.vaultDataStateFlow.value, + DataState.NoNetwork(data = null), + vaultRepository.ciphersStateFlow.value, ) assertEquals( - DataState.NoNetwork( - data = null, - ), + DataState.NoNetwork(data = null), + vaultRepository.collectionsStateFlow.value, + ) + assertEquals( + DataState.NoNetwork(data = null), + vaultRepository.foldersStateFlow.value, + ) + assertEquals( + DataState.NoNetwork(data = null), vaultRepository.sendDataStateFlow.value, ) } @Test - fun `sync with NoNetwork data should update vaultDataStateFlow to NoNetwork with data`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val mockSyncResponse = createMockSyncResponse(number = 1) - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultDiskSource.replaceVaultData( - userId = MOCK_USER_STATE.activeUserId, - vault = mockSyncResponse, - ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() + fun `sync with NoNetwork should update vaultDataStateFlow to NoNetwork`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { syncService.sync() } returns UnknownHostException().asFailure() + setupVaultDiskSourceFlows() - vaultRepository.vaultDataStateFlow.test { - assertEquals( - DataState.Loading, - awaitItem(), - ) + vaultRepository + .vaultDataStateFlow + .test { + assertEquals(DataState.Loading, awaitItem()) vaultRepository.sync() - assertEquals( - DataState.Loaded( - data = VaultData( - cipherViewList = listOf(createMockCipherView(number = 1)), - collectionViewList = listOf(createMockCollectionView(number = 1)), - folderViewList = listOf(createMockFolderView(number = 1)), - ), - ), - awaitItem(), - ) - coEvery { - syncService.sync() - } returns UnknownHostException().asFailure() - vaultRepository.sync() - assertEquals( - DataState.Pending( - data = VaultData( - cipherViewList = listOf(createMockCipherView(number = 1)), - collectionViewList = listOf(createMockCollectionView(number = 1)), - folderViewList = listOf(createMockFolderView(number = 1)), - ), - ), - awaitItem(), - ) - assertEquals( - DataState.NoNetwork( - data = VaultData( - cipherViewList = listOf(createMockCipherView(number = 1)), - collectionViewList = listOf(createMockCollectionView(number = 1)), - folderViewList = listOf(createMockFolderView(number = 1)), - ), - ), - awaitItem(), - ) + assertEquals(DataState.NoNetwork(data = null), awaitItem()) } - } + } @Test fun `sync with NoNetwork data should update sendDataStateFlow to NoNetwork with data`() = @@ -798,15 +528,6 @@ class VaultRepositoryTest { vault = mockSyncResponse, ) } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() coEvery { vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) } returns listOf(createMockSendView(number = 1)).asSuccess() @@ -912,15 +633,6 @@ class VaultRepositoryTest { vault = mockSyncResponse, ) } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() coEvery { vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) } returns listOf(createMockSendView(number = 1)).asSuccess() @@ -992,15 +704,6 @@ class VaultRepositoryTest { vault = mockSyncResponse, ) } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() coEvery { vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) } returns listOf(createMockSendView(number = 1)).asSuccess() @@ -1044,12 +747,6 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() fakeAuthDiskSource.storePrivateKey( userId = "mockId-1", privateKey = "mockPrivateKey-1", @@ -1093,12 +790,6 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns mockk() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns mockk() fakeAuthDiskSource.storePrivateKey( userId = "mockId-1", privateKey = "mockPrivateKey-1", @@ -1151,12 +842,6 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns mockk() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns mockk() fakeAuthDiskSource.storePrivateKey( userId = "mockId-1", privateKey = "mockPrivateKey-1", @@ -1219,12 +904,6 @@ class VaultRepositoryTest { fun `unlockVaultAndSyncForCurrentUser with unlockVault AuthenticationError for users should return AuthenticationError`() = runTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns mockk() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns mockk() fakeAuthDiskSource.storePrivateKey( userId = "mockId-1", privateKey = "mockPrivateKey-1", @@ -1272,12 +951,6 @@ class VaultRepositoryTest { fun `unlockVaultAndSyncForCurrentUser with unlockVault AuthenticationError for orgs should return AuthenticationError`() = runTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns mockk() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns mockk() fakeAuthDiskSource.storePrivateKey( userId = "mockId-1", privateKey = "mockPrivateKey-1", @@ -1821,132 +1494,61 @@ class VaultRepositoryTest { } @Test - fun `clearUnlockedData should update the vaultDataStateFlow to Loading`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val mockSyncResponse = createMockSyncResponse(number = 1) - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultDiskSource.replaceVaultData( - userId = MOCK_USER_STATE.activeUserId, - vault = mockSyncResponse, - ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.vaultDataStateFlow.test { - assertEquals( - DataState.Loading, - awaitItem(), - ) - vaultRepository.sync() - assertEquals( - DataState.Loaded( - data = VaultData( - cipherViewList = listOf(createMockCipherView(number = 1)), - collectionViewList = listOf(createMockCollectionView(number = 1)), - folderViewList = listOf(createMockFolderView(number = 1)), - ), - ), - awaitItem(), - ) - - vaultRepository.clearUnlockedData() - - assertEquals( - DataState.Loading, - awaitItem(), - ) - } - } - - @Test - fun `clearUnlockedData should update the sendDataStateFlow to Loading`() = - runTest { - val mockSyncResponse = createMockSyncResponse(number = 1) - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultDiskSource.replaceVaultData( - userId = MOCK_USER_STATE.activeUserId, - vault = mockSyncResponse, - ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.sendDataStateFlow.test { - assertEquals( - DataState.Loading, - awaitItem(), - ) - vaultRepository.sync() - assertEquals( - DataState.Loaded( - data = SendData( - sendViewList = listOf(createMockSendView(number = 1)), - ), - ), - awaitItem(), - ) - - vaultRepository.clearUnlockedData() - - assertEquals( - DataState.Loading, - awaitItem(), - ) - } - } - - @Test - fun `getVaultItemStateFlow should receive updates whenever a sync is called`() = runTest { - val itemId = 1234 - val itemIdString = "mockId-$itemId" - val item = createMockCipherView(itemId) + fun `clearUnlockedData should update the vaultDataStateFlow to Loading`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - val mockSyncResponse = createMockSyncResponse(number = itemId) + coEvery { + vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) + } returns listOf(createMockCipherView(number = 1)).asSuccess() + coEvery { + vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) + } returns listOf(createMockFolderView(number = 1)).asSuccess() + coEvery { + vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) + } returns listOf(createMockCollectionView(number = 1)).asSuccess() + coEvery { + vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) + } returns listOf(createMockSendView(number = 1)).asSuccess() + val ciphersFlow = bufferedMutableSharedFlow>() + val collectionsFlow = bufferedMutableSharedFlow>() + val foldersFlow = bufferedMutableSharedFlow>() + setupVaultDiskSourceFlows( + ciphersFlow = ciphersFlow, + collectionsFlow = collectionsFlow, + foldersFlow = foldersFlow, + ) + + vaultRepository.vaultDataStateFlow.test { + assertEquals(DataState.Loading, awaitItem()) + + ciphersFlow.tryEmit(listOf(createMockCipher(number = 1))) + collectionsFlow.tryEmit(listOf(createMockCollection(number = 1))) + foldersFlow.tryEmit(listOf(createMockFolder(number = 1))) + + assertEquals( + DataState.Loaded( + data = VaultData( + cipherViewList = listOf(createMockCipherView(number = 1)), + collectionViewList = listOf(createMockCollectionView(number = 1)), + folderViewList = listOf(createMockFolderView(number = 1)), + ), + ), + awaitItem(), + ) + + vaultRepository.clearUnlockedData() + + assertEquals(DataState.Loading, awaitItem()) + } + } + + @Test + fun `clearUnlockedData should update the sendDataStateFlow to Loading`() = runTest { + val mockSyncResponse = createMockSyncResponse(number = 1) coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() coEvery { vaultSdkSource.initializeOrganizationCrypto( request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(itemId), + organizationKeys = createMockOrganizationKeys(1), ), ) } returns InitializeCryptoResult.Success.asSuccess() @@ -1957,32 +1559,31 @@ class VaultRepositoryTest { ) } just runs coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(itemId))) - } returns listOf(item).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(itemId))) - } returns listOf(createMockFolderView(1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(itemId))) - } returns listOf(createMockCollectionView(itemId)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(itemId))) - } returns listOf(createMockSendView(itemId)).asSuccess() + vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) + } returns listOf(createMockSendView(number = 1)).asSuccess() + fakeAuthDiskSource.userState = MOCK_USER_STATE - vaultRepository.getVaultItemStateFlow(itemIdString).test { - assertEquals(DataState.Loading, awaitItem()) + vaultRepository.sendDataStateFlow.test { + assertEquals( + DataState.Loading, + awaitItem(), + ) vaultRepository.sync() - assertEquals(DataState.Loaded(item), awaitItem()) - vaultRepository.sync() - assertEquals(DataState.Pending(item), awaitItem()) - assertEquals(DataState.Loaded(item), awaitItem()) - } + assertEquals( + DataState.Loaded( + data = SendData( + sendViewList = listOf(createMockSendView(number = 1)), + ), + ), + awaitItem(), + ) - coVerify(exactly = 2) { - syncService.sync() - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(itemId))) - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(itemId))) - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(itemId))) + vaultRepository.clearUnlockedData() + + assertEquals( + DataState.Loading, + awaitItem(), + ) } } @@ -1992,9 +1593,8 @@ class VaultRepositoryTest { val folderIdString = "mockId-$folderId" val throwable = Throwable("Fail") fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { - syncService.sync() - } returns throwable.asFailure() + coEvery { syncService.sync() } returns throwable.asFailure() + setupVaultDiskSourceFlows() vaultRepository.getVaultItemStateFlow(folderIdString).test { assertEquals(DataState.Loading, awaitItem()) @@ -2013,9 +1613,8 @@ class VaultRepositoryTest { val itemId = 1234 val itemIdString = "mockId-$itemId" fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { - syncService.sync() - } returns UnknownHostException().asFailure() + coEvery { syncService.sync() } returns UnknownHostException().asFailure() + setupVaultDiskSourceFlows() vaultRepository.getVaultItemStateFlow(itemIdString).test { assertEquals(DataState.Loading, awaitItem()) @@ -2028,113 +1627,14 @@ class VaultRepositoryTest { } } - @Test - fun `getVaultItemStateFlow should update to Loaded with null when a item cannot be found`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val itemIdString = "mockId-1234" - val mockSyncResponse = createMockSyncResponse(1) - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultDiskSource.replaceVaultData( - userId = MOCK_USER_STATE.activeUserId, - vault = mockSyncResponse, - ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(1))) - } returns listOf(createMockSendView(1)).asSuccess() - - vaultRepository.getVaultItemStateFlow(itemIdString).test { - assertEquals(DataState.Loading, awaitItem()) - vaultRepository.sync() - assertEquals(DataState.Loaded(null), awaitItem()) - } - - coVerify(exactly = 1) { - syncService.sync() - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(1))) - } - } - - @Test - fun `getVaultFolderStateFlow should receive updates whenever a sync is called`() = runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val folderId = 1234 - val folderIdString = "mockId-$folderId" - val folder = createMockFolderView(folderId) - val mockSyncResponse = createMockSyncResponse(folderId) - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(folderId), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultDiskSource.replaceVaultData( - userId = MOCK_USER_STATE.activeUserId, - vault = mockSyncResponse, - ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(folderId))) - } returns listOf(createMockCipherView(folderId)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(folderId))) - } returns listOf(createMockFolderView(folderId)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(folderId))) - } returns listOf(createMockCollectionView(folderId)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(folderId))) - } returns listOf(createMockSendView(folderId)).asSuccess() - - vaultRepository.getVaultFolderStateFlow(folderIdString).test { - assertEquals(DataState.Loading, awaitItem()) - vaultRepository.sync() - assertEquals(DataState.Loaded(folder), awaitItem()) - vaultRepository.sync() - assertEquals(DataState.Pending(folder), awaitItem()) - assertEquals(DataState.Loaded(folder), awaitItem()) - } - - coVerify(exactly = 2) { - syncService.sync() - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(folderId))) - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(folderId))) - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(folderId))) - } - } - @Test fun `getVaultFolderStateFlow should update to NoNetwork when a sync fails from no network`() = runTest { val folderId = 1234 val folderIdString = "mockId-$folderId" fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { - syncService.sync() - } returns UnknownHostException().asFailure() + coEvery { syncService.sync() } returns UnknownHostException().asFailure() + setupVaultDiskSourceFlows() vaultRepository.getVaultFolderStateFlow(folderIdString).test { assertEquals(DataState.Loading, awaitItem()) @@ -2153,9 +1653,8 @@ class VaultRepositoryTest { val folderIdString = "mockId-$folderId" val throwable = Throwable("Fail") fakeAuthDiskSource.userState = MOCK_USER_STATE - coEvery { - syncService.sync() - } returns throwable.asFailure() + coEvery { syncService.sync() } returns throwable.asFailure() + setupVaultDiskSourceFlows() vaultRepository.getVaultFolderStateFlow(folderIdString).test { assertEquals(DataState.Loading, awaitItem()) @@ -2168,53 +1667,6 @@ class VaultRepositoryTest { } } - @Test - fun `getVaultFolderStateFlow should update to Loaded with null when a item cannot be found`() = - runTest { - fakeAuthDiskSource.userState = MOCK_USER_STATE - val folderIdString = "mockId-1234" - val mockSyncResponse = createMockSyncResponse(number = 1) - coEvery { syncService.sync() } returns mockSyncResponse.asSuccess() - coEvery { - vaultSdkSource.initializeOrganizationCrypto( - request = InitOrgCryptoRequest( - organizationKeys = createMockOrganizationKeys(1), - ), - ) - } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultDiskSource.replaceVaultData( - userId = MOCK_USER_STATE.activeUserId, - vault = mockSyncResponse, - ) - } just runs - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(1))) - } returns listOf(createMockSendView(1)).asSuccess() - - vaultRepository.getVaultFolderStateFlow(folderIdString).test { - assertEquals(DataState.Loading, awaitItem()) - vaultRepository.sync() - assertEquals(DataState.Loaded(null), awaitItem()) - } - - coVerify(exactly = 1) { - syncService.sync() - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(1))) - } - } - @Test fun `createCipher with encryptCipher failure should return CreateCipherResult failure`() = runTest { @@ -2276,15 +1728,6 @@ class VaultRepositoryTest { ), ) } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() coEvery { vaultSdkSource.decryptSendList(listOf(createMockSdkSend(1))) } returns listOf(createMockSendView(1)).asSuccess() @@ -2363,15 +1806,6 @@ class VaultRepositoryTest { ), ) } returns InitializeCryptoResult.Success.asSuccess() - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(1)).asSuccess() - coEvery { - vaultSdkSource.decryptCollectionList(listOf(createMockSdkCollection(1))) - } returns listOf(createMockCollectionView(number = 1)).asSuccess() coEvery { vaultSdkSource.decryptSendList(listOf(createMockSdkSend(1))) } returns listOf(createMockSendView(1)).asSuccess() @@ -2386,6 +1820,22 @@ class VaultRepositoryTest { //region Helper functions + /** + * Helper setup all flows required to properly subscribe to the + * [VaultRepository.vaultDataStateFlow]. + */ + private fun setupVaultDiskSourceFlows( + ciphersFlow: Flow> = bufferedMutableSharedFlow(), + collectionsFlow: Flow> = bufferedMutableSharedFlow(), + foldersFlow: Flow> = bufferedMutableSharedFlow(), + ) { + coEvery { vaultDiskSource.getCiphers(MOCK_USER_STATE.activeUserId) } returns ciphersFlow + coEvery { + vaultDiskSource.getCollections(MOCK_USER_STATE.activeUserId) + } returns collectionsFlow + coEvery { vaultDiskSource.getFolders(MOCK_USER_STATE.activeUserId) } returns foldersFlow + } + /** * Helper to ensures that the vault for the user with the given [userId] is unlocked. */