diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt index 955619bbda..3bcc561dcf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSource.kt @@ -23,6 +23,11 @@ interface VaultDiskSource { */ fun getFolders(userId: String): Flow> + /** + * Retrieves all sends from the data source for a given [userId]. + */ + fun getSends(userId: String): Flow> + /** * Replaces all [vault] data for a given [userId] with the new `vault`. * diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt index 396d7f4328..78f5e2412c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceImpl.kt @@ -4,9 +4,11 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FoldersDao +import com.x8bit.bitwarden.data.vault.datasource.disk.dao.SendsDao import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CipherEntity import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CollectionEntity import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity +import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -24,6 +26,7 @@ class VaultDiskSourceImpl( private val ciphersDao: CiphersDao, private val collectionsDao: CollectionsDao, private val foldersDao: FoldersDao, + private val sendsDao: SendsDao, private val json: Json, ) : VaultDiskSource { @@ -31,6 +34,7 @@ class VaultDiskSourceImpl( private val forceCollectionsFlow = bufferedMutableSharedFlow>() private val forceFolderFlow = bufferedMutableSharedFlow>() + private val forceSendFlow = bufferedMutableSharedFlow>() override fun getCiphers( userId: String, @@ -85,6 +89,21 @@ class VaultDiskSourceImpl( }, ) + override fun getSends( + userId: String, + ): Flow> = + merge( + forceSendFlow, + sendsDao + .getAllSends(userId = userId) + .map { entities -> + entities.map { entity -> + json.decodeFromString(entity.sendJson) + } + }, + ) + + @Suppress("LongMethod") override suspend fun replaceVaultData( userId: String, vault: SyncResponseJson, @@ -132,6 +151,19 @@ class VaultDiskSourceImpl( }, ) } + val deferredSends = async { + sendsDao.replaceAllSends( + userId = userId, + sends = vault.sends.orEmpty().map { send -> + SendEntity( + userId = userId, + id = send.id, + sendType = json.encodeToString(send.type), + sendJson = json.encodeToString(send), + ) + }, + ) + } // When going from 0 items to 0 items, the respective dao flow will not re-emit // So we use this to give it a little push. if (!deferredCiphers.await()) { @@ -143,6 +175,9 @@ class VaultDiskSourceImpl( if (!deferredFolders.await()) { forceFolderFlow.tryEmit(emptyList()) } + if (!deferredSends.await()) { + forceSendFlow.tryEmit(emptyList()) + } } } @@ -151,10 +186,12 @@ class VaultDiskSourceImpl( val deferredCiphers = async { ciphersDao.deleteAllCiphers(userId = userId) } val deferredCollections = async { collectionsDao.deleteAllCollections(userId = userId) } val deferredFolders = async { foldersDao.deleteAllFolders(userId = userId) } + val deferredSends = async { sendsDao.deleteAllSends(userId = userId) } awaitAll( deferredCiphers, deferredCollections, deferredFolders, + deferredSends, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/SendsDao.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/SendsDao.kt new file mode 100644 index 0000000000..5a85f67ad6 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/SendsDao.kt @@ -0,0 +1,50 @@ +package com.x8bit.bitwarden.data.vault.datasource.disk.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity +import kotlinx.coroutines.flow.Flow + +/** + * Provides methods for inserting, retrieving, and deleting sends from the database using the + * [SendEntity]. + */ +@Dao +interface SendsDao { + + /** + * Inserts multiple sends into the database. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertSends(sends: List) + + /** + * Retrieves all sends from the database for a given [userId]. + */ + @Query("SELECT * FROM sends WHERE user_id = :userId") + fun getAllSends( + userId: String, + ): Flow> + + /** + * Deletes all the stored sends associated with the given [userId]. This will return the + * number of rows deleted by this query. + */ + @Query("DELETE FROM sends WHERE user_id = :userId") + suspend fun deleteAllSends(userId: String): Int + + /** + * Deletes all the stored sends associated with the given [userId] and then add all new + * [sends] to the database. This will return `true` if any changes were made to the database + * and `false` otherwise. + */ + @Transaction + suspend fun replaceAllSends(userId: String, sends: List): Boolean { + val deletedSendsCount = deleteAllSends(userId) + insertSends(sends) + return deletedSendsCount > 0 || sends.isNotEmpty() + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/database/VaultDatabase.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/database/VaultDatabase.kt index 72130d7ae3..e3fe18b25f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/database/VaultDatabase.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/database/VaultDatabase.kt @@ -7,9 +7,11 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTyp import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FoldersDao +import com.x8bit.bitwarden.data.vault.datasource.disk.dao.SendsDao import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CipherEntity import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CollectionEntity import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity +import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity /** * Room database for storing any persisted data from the vault sync. @@ -19,8 +21,9 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity CipherEntity::class, CollectionEntity::class, FolderEntity::class, + SendEntity::class, ], - version = 1, + version = 2, ) @TypeConverters(ZonedDateTimeTypeConverter::class) abstract class VaultDatabase : RoomDatabase() { @@ -39,4 +42,9 @@ abstract class VaultDatabase : RoomDatabase() { * Provides the DAO for accessing folder data. */ abstract fun folderDao(): FoldersDao + + /** + * Provides the DAO for accessing send data. + */ + abstract fun sendsDao(): SendsDao } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/di/VaultDiskModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/di/VaultDiskModule.kt index b9a4769ff8..8a5772d5e9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/di/VaultDiskModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/di/VaultDiskModule.kt @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTyp import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FoldersDao +import com.x8bit.bitwarden.data.vault.datasource.disk.dao.SendsDao import com.x8bit.bitwarden.data.vault.datasource.disk.database.VaultDatabase import dagger.Module import dagger.Provides @@ -32,6 +33,7 @@ class VaultDiskModule { klass = VaultDatabase::class.java, name = "vault_database", ) + .fallbackToDestructiveMigration() .addTypeConverter(ZonedDateTimeTypeConverter()) .build() @@ -47,17 +49,23 @@ class VaultDiskModule { @Singleton fun provideFolderDao(database: VaultDatabase): FoldersDao = database.folderDao() + @Provides + @Singleton + fun provideSendDao(database: VaultDatabase): SendsDao = database.sendsDao() + @Provides @Singleton fun provideVaultDiskSource( ciphersDao: CiphersDao, collectionsDao: CollectionsDao, foldersDao: FoldersDao, + sendsDao: SendsDao, json: Json, ): VaultDiskSource = VaultDiskSourceImpl( ciphersDao = ciphersDao, collectionsDao = collectionsDao, foldersDao = foldersDao, + sendsDao = sendsDao, json = json, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/SendEntity.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/SendEntity.kt new file mode 100644 index 0000000000..dcb819ea51 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/SendEntity.kt @@ -0,0 +1,24 @@ +package com.x8bit.bitwarden.data.vault.datasource.disk.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Entity representing a send in the database. + */ +@Entity(tableName = "sends") +data class SendEntity( + @PrimaryKey(autoGenerate = false) + @ColumnInfo(name = "id") + val id: String, + + @ColumnInfo(name = "user_id", index = true) + val userId: String, + + @ColumnInfo(name = "send_type") + val sendType: String, + + @ColumnInfo(name = "send_json") + val sendJson: String, +) 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 7e19337fdb..19289546c0 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 @@ -164,6 +164,12 @@ class VaultRepositoryImpl( observeVaultDiskCollections(activeUserId) } .launchIn(unconfinedScope) + // Setup sends MutableStateFlow + mutableSendDataStateFlow + .observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId -> + observeVaultDiskSends(activeUserId) + } + .launchIn(unconfinedScope) } override fun clearUnlockedData() { @@ -201,7 +207,6 @@ class VaultRepositoryImpl( unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse) storeProfileData(syncResponse = syncResponse) vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse) - decryptSendsAndUpdateSendDataState(sendList = syncResponse.sends) }, onFailure = { throwable -> mutableCiphersStateFlow.update { currentState -> @@ -474,20 +479,6 @@ class VaultRepositoryImpl( ) } - private suspend fun decryptSendsAndUpdateSendDataState(sendList: List?) { - val newState = vaultSdkSource - .decryptSendList( - sendList = sendList - .orEmpty() - .toEncryptedSdkSendList(), - ) - .fold( - onSuccess = { DataState.Loaded(data = SendData(sendViewList = it)) }, - onFailure = { DataState.Error(error = it) }, - ) - mutableSendDataStateFlow.update { newState } - } - private fun observeVaultDiskCiphers( userId: String, ): Flow>> = @@ -535,6 +526,22 @@ class VaultRepositoryImpl( ) } .onEach { mutableCollectionsStateFlow.value = it } + + private fun observeVaultDiskSends( + userId: String, + ): Flow> = + vaultDiskSource + .getSends(userId = userId) + .onStart { mutableSendDataStateFlow.value = DataState.Loading } + .map { + vaultSdkSource + .decryptSendList(sendList = it.toEncryptedSdkSendList()) + .fold( + onSuccess = { sends -> DataState.Loaded(SendData(sends)) }, + onFailure = { throwable -> DataState.Error(throwable) }, + ) + } + .onEach { mutableSendDataStateFlow.value = it } } private fun Throwable.toNetworkOrErrorState(data: T?): DataState = diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt index f82b4984ce..b7f511eb19 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt @@ -6,13 +6,16 @@ import com.x8bit.bitwarden.data.util.assertJsonEquals import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FakeCiphersDao import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FakeCollectionsDao import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FakeFoldersDao +import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FakeSendsDao import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CipherEntity import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CollectionEntity import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity +import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder +import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest @@ -29,6 +32,7 @@ class VaultDiskSourceTest { private lateinit var ciphersDao: FakeCiphersDao private lateinit var collectionsDao: FakeCollectionsDao private lateinit var foldersDao: FakeFoldersDao + private lateinit var sendsDao: FakeSendsDao private lateinit var vaultDiskSource: VaultDiskSource @@ -37,10 +41,12 @@ class VaultDiskSourceTest { ciphersDao = FakeCiphersDao() collectionsDao = FakeCollectionsDao() foldersDao = FakeFoldersDao() + sendsDao = FakeSendsDao() vaultDiskSource = VaultDiskSourceImpl( ciphersDao = ciphersDao, collectionsDao = collectionsDao, foldersDao = foldersDao, + sendsDao = sendsDao, json = json, ) } @@ -87,17 +93,30 @@ class VaultDiskSourceTest { } } + @Test + fun `getSends should emit all SendsDao updates`() = runTest { + val sendEntities = listOf(SEND_ENTITY) + val sends = listOf(SEND_1) + + vaultDiskSource + .getSends(USER_ID) + .test { + assertEquals(emptyList(), awaitItem()) + sendsDao.insertSends(sendEntities) + assertEquals(sends, awaitItem()) + } + } + @Test fun `replaceVaultData should clear the daos and insert the new vault data`() = runTest { assertEquals(ciphersDao.storedCiphers, emptyList()) assertEquals(collectionsDao.storedCollections, emptyList()) assertEquals(foldersDao.storedFolders, emptyList()) + assertEquals(sendsDao.storedSends, emptyList()) vaultDiskSource.replaceVaultData(USER_ID, VAULT_DATA) assertEquals(1, ciphersDao.storedCiphers.size) - assertEquals(1, foldersDao.storedFolders.size) - // Verify the ciphers dao is updated val storedCipherEntity = ciphersDao.storedCiphers.first() // We cannot compare the JSON strings directly because of formatting differences @@ -110,6 +129,14 @@ class VaultDiskSourceTest { // Verify the folders dao is updated assertEquals(listOf(FOLDER_ENTITY), foldersDao.storedFolders) + + assertEquals(1, sendsDao.storedSends.size) + // Verify the ciphers dao is updated + val storedSendEntity = sendsDao.storedSends.first() + // We cannot compare the JSON strings directly because of formatting differences + // So we split that off into its own assertion. + assertEquals(SEND_ENTITY.copy(sendJson = ""), storedSendEntity.copy(sendJson = "")) + assertJsonEquals(SEND_ENTITY.sendJson, storedSendEntity.sendJson) } @Test @@ -117,10 +144,12 @@ class VaultDiskSourceTest { assertFalse(ciphersDao.deleteCiphersCalled) assertFalse(collectionsDao.deleteCollectionsCalled) assertFalse(foldersDao.deleteFoldersCalled) + assertFalse(sendsDao.deleteSendsCalled) vaultDiskSource.deleteVaultData(USER_ID) assertTrue(ciphersDao.deleteCiphersCalled) assertTrue(collectionsDao.deleteCollectionsCalled) assertTrue(foldersDao.deleteFoldersCalled) + assertTrue(sendsDao.deleteSendsCalled) } } @@ -129,6 +158,7 @@ private const val USER_ID: String = "test_user_id" private val CIPHER_1: SyncResponseJson.Cipher = createMockCipher(1) private val COLLECTION_1: SyncResponseJson.Collection = createMockCollection(3) private val FOLDER_1: SyncResponseJson.Folder = createMockFolder(2) +private val SEND_1: SyncResponseJson.Send = createMockSend(1) private val VAULT_DATA: SyncResponseJson = SyncResponseJson( folders = listOf(FOLDER_1), @@ -142,7 +172,7 @@ private val VAULT_DATA: SyncResponseJson = SyncResponseJson( globalEquivalentDomains = null, equivalentDomains = null, ), - sends = null, + sends = listOf(SEND_1), ) private const val CIPHER_JSON = """ @@ -260,3 +290,39 @@ private val FOLDER_ENTITY = FolderEntity( name = "mockName-2", revisionDate = ZonedDateTime.parse("2023-10-27T12:00Z"), ) + +private const val SEND_JSON = """ +{ + "accessCount": 1, + "notes": "mockNotes-1", + "revisionDate": "2023-10-27T12:00:00.000Z", + "maxAccessCount": 1, + "hideEmail": false, + "type": 1, + "accessId": "mockAccessId-1", + "password": "mockPassword-1", + "file": { + "fileName": "mockFileName-1", + "size": 1, + "sizeName": "mockSizeName-1", + "id": "mockId-1" + }, + "deletionDate": "2023-10-27T12:00:00.000Z", + "name": "mockName-1", + "disabled": false, + "id": "mockId-1", + "text": { + "hidden": false, + "text": "mockText-1" + }, + "key": "mockKey-1", + "expirationDate": "2023-10-27T12:00:00.000Z" +} +""" + +private val SEND_ENTITY = SendEntity( + id = "mockId-1", + userId = USER_ID, + sendType = "1", + sendJson = SEND_JSON, +) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeSendsDao.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeSendsDao.kt new file mode 100644 index 0000000000..1d798c46af --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeSendsDao.kt @@ -0,0 +1,42 @@ +package com.x8bit.bitwarden.data.vault.datasource.disk.dao + +import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow +import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class FakeSendsDao : SendsDao { + + val storedSends = mutableListOf() + + var deleteSendsCalled: Boolean = false + + private val sendsFlow = bufferedMutableSharedFlow>(replay = 1) + + init { + sendsFlow.tryEmit(emptyList()) + } + + override suspend fun deleteAllSends(userId: String): Int { + deleteSendsCalled = true + val count = storedSends.count { it.userId == userId } + storedSends.removeAll { it.userId == userId } + sendsFlow.tryEmit(storedSends.toList()) + return count + } + + override fun getAllSends(userId: String): Flow> = + sendsFlow.map { ciphers -> ciphers.filter { it.userId == userId } } + + override suspend fun insertSends(sends: List) { + storedSends.addAll(sends) + sendsFlow.tryEmit(storedSends.toList()) + } + + override suspend fun replaceAllSends(userId: String, sends: List): Boolean { + val removed = storedSends.removeAll { it.userId == userId } + storedSends.addAll(sends) + sendsFlow.tryEmit(storedSends.toList()) + return removed || sends.isNotEmpty() + } +} 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 5d07103567..76a1d37469 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 @@ -28,6 +28,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollect import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganizationKeys +import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService @@ -50,6 +51,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList +import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList import io.mockk.awaits import io.mockk.coEvery import io.mockk.coVerify @@ -234,6 +236,55 @@ class VaultRepositoryTest { } } + @Test + fun `sendDataStateFlow should emit decrypted list of sends when decryptSendsList succeeds`() = + runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val mockSendList = listOf(createMockSend(number = 1)) + val mockEncryptedSendList = mockSendList.toEncryptedSdkSendList() + val mockSendViewList = listOf(createMockSendView(number = 1)) + val mutableSendsStateFlow = + bufferedMutableSharedFlow>(replay = 1) + every { + vaultDiskSource.getSends(userId = MOCK_USER_STATE.activeUserId) + } returns mutableSendsStateFlow + coEvery { + vaultSdkSource.decryptSendList(mockEncryptedSendList) + } returns mockSendViewList.asSuccess() + + vaultRepository + .sendDataStateFlow + .test { + assertEquals(DataState.Loading, awaitItem()) + mutableSendsStateFlow.tryEmit(mockSendList) + assertEquals(DataState.Loaded(SendData(mockSendViewList)), awaitItem()) + } + } + + @Test + fun `sendDataStateFlow should emit an error when decryptSendsList fails`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val throwable = Throwable("Fail") + val mockSendList = listOf(createMockSend(number = 1)) + val mockEncryptedSendList = mockSendList.toEncryptedSdkSendList() + val mutableSendsStateFlow = + bufferedMutableSharedFlow>(replay = 1) + every { + vaultDiskSource.getSends(userId = MOCK_USER_STATE.activeUserId) + } returns mutableSendsStateFlow + coEvery { + vaultSdkSource.decryptSendList(mockEncryptedSendList) + } returns throwable.asFailure() + + vaultRepository + .sendDataStateFlow + .test { + assertEquals(DataState.Loading, awaitItem()) + mutableSendsStateFlow.tryEmit(mockSendList) + assertEquals(DataState.Error(throwable), awaitItem()) + } + } + @Test fun `deleteVaultData should call deleteVaultData on VaultDiskSource`() { val userId = "userId-1234" @@ -248,7 +299,7 @@ class VaultRepositoryTest { @Suppress("MaxLineLength") @Test - fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and sendDataStateFlows`() = + fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and VaultDiskSource`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val mockSyncResponse = createMockSyncResponse(number = 1) @@ -266,10 +317,6 @@ class VaultRepositoryTest { vault = mockSyncResponse, ) } just runs - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE vaultRepository.sync() @@ -303,14 +350,6 @@ class VaultRepositoryTest { userId = "mockId-1", organizations = listOf(createMockOrganization(number = 1)), ) - assertEquals( - DataState.Loaded( - data = SendData( - sendViewList = listOf(createMockSendView(number = 1)), - ), - ), - vaultRepository.sendDataStateFlow.value, - ) coVerify { vaultDiskSource.replaceVaultData( userId = MOCK_USER_STATE.activeUserId, @@ -325,121 +364,30 @@ class VaultRepositoryTest { } @Test - fun `sync with data should update sendDataStateFlow to Pending before service sync`() = - 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.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE + 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() - vaultRepository.sendDataStateFlow.test { - assertEquals( - DataState.Loading, - awaitItem(), - ) - vaultRepository.sync() - assertEquals( - DataState.Loaded( - data = SendData( - sendViewList = listOf(createMockSendView(number = 1)), - ), - ), - awaitItem(), - ) - vaultRepository.sync() - assertEquals( - DataState.Pending( - data = SendData( - sendViewList = listOf(createMockSendView(number = 1)), - ), - ), - awaitItem(), - ) - assertEquals( - DataState.Loaded( - data = SendData( - sendViewList = listOf(createMockSendView(number = 1)), - ), - ), - awaitItem(), - ) - } - } + vaultRepository.sync() - @Test - fun `sync with decryptSendList Failure should update sendDataStateFlows 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.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns mockException.asFailure() - fakeAuthDiskSource.userState = MOCK_USER_STATE - - vaultRepository.sync() - - assertEquals( - DataState.Error(error = mockException), - vaultRepository.sendDataStateFlow.value, - ) - } - - @Test - 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() - - vaultRepository.sync() - - assertEquals( - DataState.Error>(mockException), - vaultRepository.ciphersStateFlow.value, - ) - assertEquals( - DataState.Error>(mockException), - vaultRepository.collectionsStateFlow.value, - ) - assertEquals( - DataState.Error>(mockException), - vaultRepository.foldersStateFlow.value, - ) - assertEquals( - DataState.Error(mockException), - vaultRepository.sendDataStateFlow.value, - ) - } + assertEquals( + DataState.Error>(mockException), + vaultRepository.ciphersStateFlow.value, + ) + assertEquals( + DataState.Error>(mockException), + vaultRepository.collectionsStateFlow.value, + ) + assertEquals( + DataState.Error>(mockException), + vaultRepository.foldersStateFlow.value, + ) + assertEquals( + DataState.Error(mockException), + vaultRepository.sendDataStateFlow.value, + ) + } @Test fun `sync with syncService Failure should update vaultDataStateFlow with an Error`() = runTest { @@ -497,66 +445,37 @@ class VaultRepositoryTest { } } + @Suppress("MaxLineLength") @Test - fun `sync with NoNetwork data should update sendDataStateFlow to NoNetwork with data`() = + fun `sync with NoNetwork data should update sendDataStateFlow to Pending and NoNetwork with data`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - val mockSyncResponse = createMockSyncResponse(number = 1) + coEvery { syncService.sync() } returns UnknownHostException().asFailure() + val sendsFlow = bufferedMutableSharedFlow>() + setupVaultDiskSourceFlows(sendsFlow = sendsFlow) coEvery { - syncService.sync() - } returnsMany listOf( - mockSyncResponse.asSuccess(), - UnknownHostException().asFailure(), - ) - 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.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() + vaultSdkSource.decryptSendList(listOf(createMockSdkSend(1))) + } returns listOf(createMockSendView(1)).asSuccess() - vaultRepository.sendDataStateFlow.test { - assertEquals( - DataState.Loading, - awaitItem(), - ) - vaultRepository.sync() - assertEquals( - DataState.Loaded( - data = SendData( - sendViewList = listOf(createMockSendView(number = 1)), - ), - ), - awaitItem(), - ) - vaultRepository.sync() - assertEquals( - DataState.Pending( - data = SendData( - sendViewList = listOf(createMockSendView(number = 1)), - ), - ), - awaitItem(), - ) - assertEquals( - DataState.NoNetwork( - data = SendData( - sendViewList = listOf(createMockSendView(number = 1)), - ), - ), - awaitItem(), - ) - } + vaultRepository + .sendDataStateFlow + .test { + assertEquals(DataState.Loading, awaitItem()) + sendsFlow.tryEmit(listOf(createMockSend(1))) + assertEquals( + DataState.Loaded(data = SendData(listOf(createMockSendView(1)))), + awaitItem(), + ) + vaultRepository.sync() + assertEquals( + DataState.Pending(data = SendData(listOf(createMockSendView(1)))), + awaitItem(), + ) + assertEquals( + DataState.NoNetwork(data = SendData(listOf(createMockSendView(1)))), + awaitItem(), + ) + } } @Suppress("MaxLineLength") @@ -1536,47 +1455,28 @@ class VaultRepositoryTest { @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 + fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) } returns listOf(createMockSendView(number = 1)).asSuccess() - fakeAuthDiskSource.userState = MOCK_USER_STATE + val sendsFlow = bufferedMutableSharedFlow>() + setupVaultDiskSourceFlows(sendsFlow = sendsFlow) vaultRepository.sendDataStateFlow.test { - assertEquals( - DataState.Loading, - awaitItem(), - ) - vaultRepository.sync() + assertEquals(DataState.Loading, awaitItem()) + + sendsFlow.tryEmit(listOf(createMockSend(number = 1))) + assertEquals( DataState.Loaded( - data = SendData( - sendViewList = listOf(createMockSendView(number = 1)), - ), + data = SendData(sendViewList = listOf(createMockSendView(number = 1))), ), awaitItem(), ) vaultRepository.clearUnlockedData() - assertEquals( - DataState.Loading, - awaitItem(), - ) + assertEquals(DataState.Loading, awaitItem()) } } @@ -1857,12 +1757,14 @@ class VaultRepositoryTest { ciphersFlow: Flow> = bufferedMutableSharedFlow(), collectionsFlow: Flow> = bufferedMutableSharedFlow(), foldersFlow: Flow> = bufferedMutableSharedFlow(), + sendsFlow: 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 + coEvery { vaultDiskSource.getSends(MOCK_USER_STATE.activeUserId) } returns sendsFlow } /**