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 9274b7864..0dba9869f 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 @@ -367,8 +367,8 @@ class VaultRepositoryImpl( userId = userId, policies = syncResponse.policies, ) - vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse) settingsDiskSource.storeLastSyncTime(userId = userId, clock.instant()) + vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse) }, onFailure = { throwable -> updateVaultStateFlowsToError(throwable) @@ -1335,6 +1335,7 @@ class VaultRepositoryImpl( onFailure = { throwable -> DataState.Error(throwable) }, ) } + .map { it.orLoadingIfNotSynced(userId = userId) } .onEach { mutableCiphersStateFlow.value = it } private fun observeVaultDiskDomains( @@ -1368,6 +1369,7 @@ class VaultRepositoryImpl( onFailure = { throwable -> DataState.Error(throwable) }, ) } + .map { it.orLoadingIfNotSynced(userId = userId) } .onEach { mutableFoldersStateFlow.value = it } private fun observeVaultDiskCollections( @@ -1392,6 +1394,7 @@ class VaultRepositoryImpl( onFailure = { throwable -> DataState.Error(throwable) }, ) } + .map { it.orLoadingIfNotSynced(userId = userId) } .onEach { mutableCollectionsStateFlow.value = it } private fun observeVaultDiskSends( @@ -1408,10 +1411,12 @@ class VaultRepositoryImpl( sendList = it.toEncryptedSdkSendList(), ) .fold( - onSuccess = { sends -> DataState.Loaded(SendData(sends)) }, + onSuccess = { sends -> DataState.Loaded(sends) }, onFailure = { throwable -> DataState.Error(throwable) }, ) } + .map { it.orLoadingIfNotSynced(userId = userId) } + .map { dataState -> dataState.map { SendData(it) } } .onEach { mutableSendDataStateFlow.value = it } private fun updateVaultStateFlowsToError(throwable: Throwable) { @@ -1442,6 +1447,20 @@ class VaultRepositoryImpl( } } + /** + * Returns the given [DataState] as-is, or [DataState.Loading] if vault data for the given + * [userId] has not synced. This can be used to distinguish between empty data in the database + * because we are in the process of syncing from legitimately having no vault data. + */ + private fun DataState>.orLoadingIfNotSynced( + userId: String, + ): DataState> = + this + .takeUnless { + settingsDiskSource.getLastSyncTime(userId = userId) == null + } + ?: DataState.Loading + //region Push notification helpers /** * Deletes the cipher specified by [syncCipherDeleteData] from disk. 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 c651fe514..a4386b333 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 @@ -1573,6 +1573,103 @@ class VaultRepositoryTest { } } + @Test + fun `vaultDataStateFlow should return empty when last sync time is populated`() = + runTest { + val userId = "mockId-1" + coEvery { + vaultLockManager.waitUntilUnlocked(userId = userId) + } just runs + every { + settingsDiskSource.getLastSyncTime(userId = userId) + } returns clock.instant() + + fakeAuthDiskSource.userState = MOCK_USER_STATE + setupEmptyDecryptionResults() + setupVaultDiskSourceFlows( + ciphersFlow = flowOf(emptyList()), + collectionsFlow = flowOf(emptyList()), + domainsFlow = flowOf( + SyncResponseJson.Domains( + globalEquivalentDomains = emptyList(), + equivalentDomains = emptyList(), + ), + ), + foldersFlow = flowOf(emptyList()), + sendsFlow = flowOf(emptyList()), + ) + + turbineScope { + val ciphersStateFlow = vaultRepository.ciphersStateFlow.testIn(backgroundScope) + val collectionsStateFlow = + vaultRepository.collectionsStateFlow.testIn(backgroundScope) + val foldersStateFlow = vaultRepository.foldersStateFlow.testIn(backgroundScope) + val sendsStateFlow = vaultRepository.sendDataStateFlow.testIn(backgroundScope) + val domainsStateFlow = vaultRepository.domainsStateFlow.testIn(backgroundScope) + + assertEquals( + DataState.Loaded(emptyList()), + ciphersStateFlow.awaitItem(), + ) + assertEquals( + DataState.Loaded(emptyList()), + collectionsStateFlow.awaitItem(), + ) + assertEquals( + DataState.Loaded(emptyList()), + foldersStateFlow.awaitItem(), + ) + assertEquals( + DataState.Loaded(SendData(sendViewList = emptyList())), + sendsStateFlow.awaitItem(), + ) + assertEquals( + DataState.Loaded( + DomainsData( + equivalentDomains = emptyList(), + globalEquivalentDomains = emptyList(), + ), + ), + domainsStateFlow.awaitItem(), + ) + } + } + + @Test + fun `vaultDataStateFlow should return loading when last sync time is null`() = + runTest { + val userId = "mockId-1" + coEvery { + vaultLockManager.waitUntilUnlocked(userId = userId) + } just runs + every { + settingsDiskSource.getLastSyncTime(userId = userId) + } returns null + fakeAuthDiskSource.userState = MOCK_USER_STATE + setupEmptyDecryptionResults() + setupVaultDiskSourceFlows( + ciphersFlow = flowOf(emptyList()), + collectionsFlow = flowOf(emptyList()), + domainsFlow = flowOf(), + foldersFlow = flowOf(emptyList()), + sendsFlow = flowOf(emptyList()), + ) + turbineScope { + val ciphersStateFlow = vaultRepository.ciphersStateFlow.testIn(backgroundScope) + val collectionsStateFlow = + vaultRepository.collectionsStateFlow.testIn(backgroundScope) + val foldersStateFlow = vaultRepository.foldersStateFlow.testIn(backgroundScope) + val sendsStateFlow = vaultRepository.sendDataStateFlow.testIn(backgroundScope) + val domainsStateFlow = vaultRepository.domainsStateFlow.testIn(backgroundScope) + + assertEquals(DataState.Loading, ciphersStateFlow.awaitItem()) + assertEquals(DataState.Loading, collectionsStateFlow.awaitItem()) + assertEquals(DataState.Loading, foldersStateFlow.awaitItem()) + assertEquals(DataState.Loading, sendsStateFlow.awaitItem()) + assertEquals(DataState.Loading, domainsStateFlow.awaitItem()) + } + } + @Test fun `getVaultFolderStateFlow should update to NoNetwork when a sync fails from no network`() = runTest { @@ -5463,6 +5560,34 @@ class VaultRepositoryTest { coEvery { vaultDiskSource.getSends(MOCK_USER_STATE.activeUserId) } returns sendsFlow } + private fun setupEmptyDecryptionResults() { + coEvery { + vaultSdkSource.decryptCipherList( + userId = MOCK_USER_STATE.activeUserId, + cipherList = emptyList(), + ) + } returns emptyList().asSuccess() + coEvery { + vaultSdkSource.decryptFolderList( + userId = MOCK_USER_STATE.activeUserId, + folderList = emptyList(), + ) + } returns emptyList().asSuccess() + coEvery { + vaultSdkSource.decryptCollectionList( + userId = MOCK_USER_STATE.activeUserId, + collectionList = emptyList(), + ) + } returns emptyList().asSuccess() + + coEvery { + vaultSdkSource.decryptSendList( + userId = MOCK_USER_STATE.activeUserId, + sendList = emptyList(), + ) + } returns emptyList().asSuccess() + } + private suspend fun setupDataStateFlow(userId: String) { coEvery { vaultSdkSource.decryptCipherList(