From 52acc2fa471f7193df3bf7fdbc1fc0ebb6ad6460 Mon Sep 17 00:00:00 2001 From: Lucas Kivi <125697099+lucas-livefront@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:17:03 -0600 Subject: [PATCH] Add Domains database (#784) --- .../vault/datasource/disk/VaultDiskSource.kt | 5 ++ .../datasource/disk/VaultDiskSourceImpl.kt | 21 ++++++ .../vault/datasource/disk/dao/DomainsDao.kt | 35 +++++++++ .../datasource/disk/database/VaultDatabase.kt | 10 ++- .../datasource/disk/di/VaultDiskModule.kt | 7 ++ .../datasource/disk/entity/DomainsEntity.kt | 18 +++++ .../data/vault/repository/VaultRepository.kt | 9 +++ .../vault/repository/VaultRepositoryImpl.kt | 34 +++++++++ .../vault/repository/model/DomainsData.kt | 25 +++++++ .../repository/util/DomainsExtensions.kt | 30 ++++++++ .../datasource/disk/VaultDiskSourceTest.kt | 63 ++++++++++++++-- .../datasource/disk/dao/FakeDomainsDao.kt | 31 ++++++++ .../vault/repository/VaultRepositoryTest.kt | 46 ++++++++++++ .../vault/repository/model/DomainsDataUtil.kt | 16 +++++ .../repository/util/DomainsExtensionsTest.kt | 71 +++++++++++++++++++ 15 files changed, 416 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/DomainsDao.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/DomainsEntity.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DomainsData.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/DomainsExtensions.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeDomainsDao.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/vault/repository/model/DomainsDataUtil.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/DomainsExtensionsTest.kt 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 ca804dc17..7eb2a0d40 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 @@ -34,6 +34,11 @@ interface VaultDiskSource { */ fun getCollections(userId: String): Flow> + /** + * Retrieves all domains from the data source for a given [userId]. + */ + fun getDomains(userId: String): Flow + /** * Saves a folder to the data source for the given [userId]. */ 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 7482318c7..17e661549 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 @@ -3,10 +3,12 @@ package com.x8bit.bitwarden.data.vault.datasource.disk import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow 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.DomainsDao 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.DomainsEntity 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 @@ -16,6 +18,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -26,6 +29,7 @@ import kotlinx.serialization.json.Json class VaultDiskSourceImpl( private val ciphersDao: CiphersDao, private val collectionsDao: CollectionsDao, + private val domainsDao: DomainsDao, private val foldersDao: FoldersDao, private val sendsDao: SendsDao, private val json: Json, @@ -103,6 +107,13 @@ class VaultDiskSourceImpl( }, ) + override fun getDomains(userId: String): Flow = + domainsDao + .getDomains(userId) + .map { entity -> + json.decodeFromString(entity.domainsJson) + } + override suspend fun saveFolder(userId: String, folder: SyncResponseJson.Folder) { foldersDao.insertFolder( folder = FolderEntity( @@ -198,6 +209,14 @@ class VaultDiskSourceImpl( }, ) } + launch { + domainsDao.insertDomains( + domains = DomainsEntity( + userId = userId, + domainsJson = json.encodeToString(vault.domains), + ), + ) + } val deferredFolders = async { foldersDao.replaceAllFolders( userId = userId, @@ -245,11 +264,13 @@ class VaultDiskSourceImpl( coroutineScope { val deferredCiphers = async { ciphersDao.deleteAllCiphers(userId = userId) } val deferredCollections = async { collectionsDao.deleteAllCollections(userId = userId) } + val deferredDomains = async { domainsDao.deleteDomains(userId = userId) } val deferredFolders = async { foldersDao.deleteAllFolders(userId = userId) } val deferredSends = async { sendsDao.deleteAllSends(userId = userId) } awaitAll( deferredCiphers, deferredCollections, + deferredDomains, deferredFolders, deferredSends, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/DomainsDao.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/DomainsDao.kt new file mode 100644 index 000000000..a5977cb26 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/DomainsDao.kt @@ -0,0 +1,35 @@ +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 com.x8bit.bitwarden.data.vault.datasource.disk.entity.DomainsEntity +import kotlinx.coroutines.flow.Flow + +/** + * Provides methods for inserting, retrieving, and deleting domains from the database using the + * [DomainsEntity]. + */ +@Dao +interface DomainsDao { + /** + * Deletes the stored domains associated with the given [userId]. + */ + @Query("DELETE FROM domains WHERE user_id = :userId") + suspend fun deleteDomains(userId: String) + + /** + * Retrieves domains from the database for a given [userId]. + */ + @Query("SELECT * FROM domains WHERE user_id = :userId") + fun getDomains( + userId: String, + ): Flow + + /** + * Inserts domains into the database. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertDomains(domains: DomainsEntity) +} 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 e3fe18b25..97ef57e31 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 @@ -6,10 +6,12 @@ import androidx.room.TypeConverters import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter 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.DomainsDao 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.DomainsEntity import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity @@ -20,10 +22,11 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity entities = [ CipherEntity::class, CollectionEntity::class, + DomainsEntity::class, FolderEntity::class, SendEntity::class, ], - version = 2, + version = 3, ) @TypeConverters(ZonedDateTimeTypeConverter::class) abstract class VaultDatabase : RoomDatabase() { @@ -38,6 +41,11 @@ abstract class VaultDatabase : RoomDatabase() { */ abstract fun collectionDao(): CollectionsDao + /** + * Provides the DAO for accessing domains data. + */ + abstract fun domainsDao(): DomainsDao + /** * Provides the DAO for accessing folder data. */ 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 8a5772d5e..52a10b60a 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 @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSourceImpl import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter 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.DomainsDao 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 @@ -45,6 +46,10 @@ class VaultDiskModule { @Singleton fun provideCollectionDao(database: VaultDatabase): CollectionsDao = database.collectionDao() + @Provides + @Singleton + fun provideDomainsDao(database: VaultDatabase): DomainsDao = database.domainsDao() + @Provides @Singleton fun provideFolderDao(database: VaultDatabase): FoldersDao = database.folderDao() @@ -58,12 +63,14 @@ class VaultDiskModule { fun provideVaultDiskSource( ciphersDao: CiphersDao, collectionsDao: CollectionsDao, + domainsDao: DomainsDao, foldersDao: FoldersDao, sendsDao: SendsDao, json: Json, ): VaultDiskSource = VaultDiskSourceImpl( ciphersDao = ciphersDao, collectionsDao = collectionsDao, + domainsDao = domainsDao, foldersDao = foldersDao, sendsDao = sendsDao, json = json, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/DomainsEntity.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/DomainsEntity.kt new file mode 100644 index 000000000..e8213b1d9 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/disk/entity/DomainsEntity.kt @@ -0,0 +1,18 @@ +package com.x8bit.bitwarden.data.vault.datasource.disk.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Entity representing a set of domains in the database. + */ +@Entity(tableName = "domains") +data class DomainsEntity( + @PrimaryKey(autoGenerate = false) + @ColumnInfo(name = "user_id") + val userId: String, + + @ColumnInfo(name = "domains_json") + val domainsJson: String, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index d778eb505..5deada358 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult +import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult @@ -69,6 +70,14 @@ interface VaultRepository : VaultLockManager { */ val collectionsStateFlow: StateFlow>> + /** + * Flow that represents all domains for the active user. + * + * Note that the [StateFlow.value] will return the last known value but the [StateFlow] itself + * must be collected in order to trigger state changes. + */ + val domainsStateFlow: StateFlow> + /** * Flow that represents all folders for the active user. * 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 fe9653529..20060867d 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 @@ -46,6 +46,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult +import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult @@ -56,6 +57,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult +import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend @@ -136,6 +138,9 @@ class VaultRepositoryImpl( private val mutableCollectionsStateFlow = MutableStateFlow>>(DataState.Loading) + private val mutableDomainsStateFlow = + MutableStateFlow>(DataState.Loading) + override var vaultFilterType: VaultFilterType = VaultFilterType.AllVaults override val vaultDataStateFlow: StateFlow> = @@ -171,6 +176,9 @@ class VaultRepositoryImpl( override val ciphersStateFlow: StateFlow>> get() = mutableCiphersStateFlow.asStateFlow() + override val domainsStateFlow: StateFlow> + get() = mutableDomainsStateFlow.asStateFlow() + override val foldersStateFlow: StateFlow>> get() = mutableFoldersStateFlow.asStateFlow() @@ -187,6 +195,12 @@ class VaultRepositoryImpl( observeVaultDiskCiphers(activeUserId) } .launchIn(unconfinedScope) + // Setup domains MutableStateFlow + mutableDomainsStateFlow + .observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId -> + observeVaultDiskDomains(activeUserId) + } + .launchIn(unconfinedScope) // Setup folders MutableStateFlow mutableFoldersStateFlow .observeWhenSubscribedAndLoggedIn(authDiskSource.userStateFlow) { activeUserId -> @@ -209,6 +223,7 @@ class VaultRepositoryImpl( override fun clearUnlockedData() { mutableCiphersStateFlow.update { DataState.Loading } + mutableDomainsStateFlow.update { DataState.Loading } mutableFoldersStateFlow.update { DataState.Loading } mutableCollectionsStateFlow.update { DataState.Loading } mutableSendDataStateFlow.update { DataState.Loading } @@ -224,6 +239,7 @@ class VaultRepositoryImpl( val userId = activeUserId ?: return if (!syncJob.isCompleted || isVaultUnlocking(userId)) return mutableCiphersStateFlow.updateToPendingOrLoading() + mutableDomainsStateFlow.updateToPendingOrLoading() mutableFoldersStateFlow.updateToPendingOrLoading() mutableCollectionsStateFlow.updateToPendingOrLoading() mutableSendDataStateFlow.updateToPendingOrLoading() @@ -250,6 +266,11 @@ class VaultRepositoryImpl( data = currentState.data, ) } + mutableDomainsStateFlow.update { currentState -> + throwable.toNetworkOrErrorState( + data = currentState.data, + ) + } mutableFoldersStateFlow.update { currentState -> throwable.toNetworkOrErrorState( data = currentState.data, @@ -993,6 +1014,19 @@ class VaultRepositoryImpl( } .onEach { mutableCiphersStateFlow.value = it } + private fun observeVaultDiskDomains( + userId: String, + ): Flow> = + vaultDiskSource + .getDomains(userId = userId) + .onStart { mutableDomainsStateFlow.value = DataState.Loading } + .map { + DataState.Loaded( + data = it.toDomainsData(), + ) + } + .onEach { mutableDomainsStateFlow.value = it } + private fun observeVaultDiskFolders( userId: String, ): Flow>> = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DomainsData.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DomainsData.kt new file mode 100644 index 000000000..aeedd7af7 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/DomainsData.kt @@ -0,0 +1,25 @@ +package com.x8bit.bitwarden.data.vault.repository.model + +/** + * Model for equivalent domain details. + * + * @param equivalentDomains A list of equivalent domains to compare URIs to. + * @param globalEquivalentDomains A list of global equivalent domains to compare URIs to. + */ +data class DomainsData( + val equivalentDomains: List>, + val globalEquivalentDomains: List, +) { + /** + * Model for a group of domains that should be matched together. + * + * @property isExcluded If the global equivalent domain should be excluded. + * @property domains A list of domains that should all match a URI. + * @property type The domain type identifier. + */ + data class GlobalEquivalentDomain( + val isExcluded: Boolean, + val domains: List, + val type: Int, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/DomainsExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/DomainsExtensions.kt new file mode 100644 index 000000000..f889d724a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/DomainsExtensions.kt @@ -0,0 +1,30 @@ +package com.x8bit.bitwarden.data.vault.repository.util + +import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson.Domains +import com.x8bit.bitwarden.data.vault.repository.model.DomainsData + +/** + * Map the API [Domains] model to the internal [DomainsData] model. + */ +fun Domains.toDomainsData(): DomainsData { + val globalEquivalentDomains = this + .globalEquivalentDomains + ?.map { it.toInternalModel() } + .orEmpty() + + return DomainsData( + equivalentDomains = this.equivalentDomains.orEmpty(), + globalEquivalentDomains = globalEquivalentDomains, + ) +} + +/** + * Map the API [Domains.GlobalEquivalentDomain] model to the internal + * [DomainsData.GlobalEquivalentDomain] model. + */ +private fun Domains.GlobalEquivalentDomain.toInternalModel(): DomainsData.GlobalEquivalentDomain = + DomainsData.GlobalEquivalentDomain( + domains = this.domains.orEmpty(), + isExcluded = this.isExcluded, + type = this.type, + ) 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 e5ed68ecb..8c484171a 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 @@ -5,15 +5,18 @@ import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkMo 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.FakeDomainsDao 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.DomainsEntity 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.createMockDomains import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend import io.mockk.every @@ -21,6 +24,8 @@ import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -31,6 +36,7 @@ class VaultDiskSourceTest { private val json = PlatformNetworkModule.providesJson() private lateinit var ciphersDao: FakeCiphersDao private lateinit var collectionsDao: FakeCollectionsDao + private lateinit var domainsDao: FakeDomainsDao private lateinit var foldersDao: FakeFoldersDao private lateinit var sendsDao: FakeSendsDao @@ -40,11 +46,13 @@ class VaultDiskSourceTest { fun setup() { ciphersDao = FakeCiphersDao() collectionsDao = FakeCollectionsDao() + domainsDao = FakeDomainsDao() foldersDao = FakeFoldersDao() sendsDao = FakeSendsDao() vaultDiskSource = VaultDiskSourceImpl( ciphersDao = ciphersDao, collectionsDao = collectionsDao, + domainsDao = domainsDao, foldersDao = foldersDao, sendsDao = sendsDao, json = json, @@ -119,6 +127,17 @@ class VaultDiskSourceTest { } } + @Test + fun `getDomains should emit DomainsDao updates`() = runTest { + vaultDiskSource + .getDomains(USER_ID) + .test { + expectNoEvents() + domainsDao.insertDomains(DOMAINS_ENTITY) + assertEquals(DOMAINS_1, awaitItem()) + } + } + @Test fun `saveFolder should call insertFolder`() = runTest { assertFalse(foldersDao.insertFolderCalled) @@ -191,6 +210,7 @@ class VaultDiskSourceTest { fun `replaceVaultData should clear the daos and insert the new vault data`() = runTest { assertEquals(ciphersDao.storedCiphers, emptyList()) assertEquals(collectionsDao.storedCollections, emptyList()) + assertNull(domainsDao.storedDomains) assertEquals(foldersDao.storedFolders, emptyList()) assertEquals(sendsDao.storedSends, emptyList()) @@ -207,6 +227,17 @@ class VaultDiskSourceTest { // Verify the collections dao is updated assertEquals(listOf(COLLECTION_ENTITY), collectionsDao.storedCollections) + assertNotNull(domainsDao.storedDomains) + // Verify the domains dao is updated + val storedDomainsEntity = requireNotNull(domainsDao.storedDomains) + // We cannot compare the JSON strings directly because of formatting differences + // So we split that off into its own assertion. + assertEquals( + DOMAINS_ENTITY.copy(domainsJson = ""), + storedDomainsEntity.copy(domainsJson = ""), + ) + assertJsonEquals(DOMAINS_ENTITY.domainsJson, storedDomainsEntity.domainsJson) + // Verify the folders dao is updated assertEquals(listOf(FOLDER_ENTITY), foldersDao.storedFolders) @@ -223,11 +254,13 @@ class VaultDiskSourceTest { fun `deleteVaultData should remove all vault data matching the user ID`() = runTest { assertFalse(ciphersDao.deleteCiphersCalled) assertFalse(collectionsDao.deleteCollectionsCalled) + assertFalse(domainsDao.deleteDomainsCalled) assertFalse(foldersDao.deleteFoldersCalled) assertFalse(sendsDao.deleteSendsCalled) vaultDiskSource.deleteVaultData(USER_ID) assertTrue(ciphersDao.deleteCiphersCalled) assertTrue(collectionsDao.deleteCollectionsCalled) + assertTrue(domainsDao.deleteDomainsCalled) assertTrue(foldersDao.deleteFoldersCalled) assertTrue(sendsDao.deleteSendsCalled) } @@ -237,6 +270,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 DOMAINS_1: SyncResponseJson.Domains = createMockDomains(1) private val FOLDER_1: SyncResponseJson.Folder = createMockFolder(2) private val SEND_1: SyncResponseJson.Send = createMockSend(1) @@ -248,10 +282,7 @@ private val VAULT_DATA: SyncResponseJson = SyncResponseJson( }, ciphers = listOf(CIPHER_1), policies = null, - domains = SyncResponseJson.Domains( - globalEquivalentDomains = null, - equivalentDomains = null, - ), + domains = DOMAINS_1, sends = listOf(SEND_1), ) @@ -364,6 +395,30 @@ private val COLLECTION_ENTITY = CollectionEntity( isReadOnly = false, ) +private const val DOMAINS_JSON = """ +{ + "globalEquivalentDomains": [ + { + "excluded": false, + "domains": [ + "mockDomain-1" + ], + "type": 1 + } + ], + "equivalentDomains": [ + [ + "mockEquivalentDomain-1" + ] + ] +} +""" + +private val DOMAINS_ENTITY = DomainsEntity( + domainsJson = DOMAINS_JSON, + userId = USER_ID, +) + private val FOLDER_ENTITY = FolderEntity( id = "mockId-2", userId = USER_ID, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeDomainsDao.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeDomainsDao.kt new file mode 100644 index 000000000..675341c0c --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/disk/dao/FakeDomainsDao.kt @@ -0,0 +1,31 @@ +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.DomainsEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull + +class FakeDomainsDao : DomainsDao { + var storedDomains: DomainsEntity? = null + + var deleteDomainsCalled: Boolean = false + var getDomainsCalled: Boolean = false + var insertDomainsCalled: Boolean = false + + private val mutableDomainsFlow = bufferedMutableSharedFlow() + + override suspend fun deleteDomains(userId: String) { + deleteDomainsCalled = true + } + + override fun getDomains(userId: String): Flow { + getDomainsCalled = true + return mutableDomainsFlow.filterNotNull() + } + + override suspend fun insertDomains(domains: DomainsEntity) { + insertDomainsCalled = true + storedDomains = domains + mutableDomainsFlow.tryEmit(domains) + } +} 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 521303090..304f686f6 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 @@ -40,6 +40,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockAttachm import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCollection +import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockDomains 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 @@ -70,6 +71,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult +import com.x8bit.bitwarden.data.vault.repository.model.DomainsData import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult @@ -80,6 +82,8 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultState import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult +import com.x8bit.bitwarden.data.vault.repository.model.createMockDomainsData +import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList @@ -159,11 +163,13 @@ class VaultRepositoryTest { @BeforeEach fun setup() { + mockkStatic(SyncResponseJson.Domains::toDomainsData) mockkStatic(Uri::class) } @AfterEach fun tearDown() { + unmockkStatic(SyncResponseJson.Domains::toDomainsData) unmockkStatic(Uri::class) unmockkStatic(Instant::class) unmockkStatic(Cipher::toEncryptedNetworkCipherResponse) @@ -499,6 +505,10 @@ class VaultRepositoryTest { DataState.Error>(mockException), vaultRepository.collectionsStateFlow.value, ) + assertEquals( + DataState.Error(mockException), + vaultRepository.domainsStateFlow.value, + ) assertEquals( DataState.Error>(mockException), vaultRepository.foldersStateFlow.value, @@ -540,6 +550,10 @@ class VaultRepositoryTest { DataState.NoNetwork(data = null), vaultRepository.collectionsStateFlow.value, ) + assertEquals( + DataState.NoNetwork(data = null), + vaultRepository.domainsStateFlow.value, + ) assertEquals( DataState.NoNetwork(data = null), vaultRepository.foldersStateFlow.value, @@ -1265,6 +1279,36 @@ class VaultRepositoryTest { } } + @Test + fun `clearUnlockedData should update the domainsStateFlow to Loading`() = runTest { + fakeAuthDiskSource.userState = MOCK_USER_STATE + val domainsData = createMockDomainsData(number = 1) + coEvery { + createMockDomains(number = 1).toDomainsData() + } returns domainsData + val domainsFlow = bufferedMutableSharedFlow() + setupVaultDiskSourceFlows( + domainsFlow = domainsFlow, + ) + + vaultRepository.domainsStateFlow.test { + assertEquals(DataState.Loading, awaitItem()) + + domainsFlow.tryEmit(createMockDomains(number = 1)) + + assertEquals( + DataState.Loaded( + data = domainsData, + ), + awaitItem(), + ) + + vaultRepository.clearUnlockedData() + + assertEquals(DataState.Loading, awaitItem()) + } + } + @Test fun `getVaultItemStateFlow should update to Error when a sync fails generically`() = runTest { @@ -2927,6 +2971,7 @@ class VaultRepositoryTest { private fun setupVaultDiskSourceFlows( ciphersFlow: Flow> = bufferedMutableSharedFlow(), collectionsFlow: Flow> = bufferedMutableSharedFlow(), + domainsFlow: Flow = bufferedMutableSharedFlow(), foldersFlow: Flow> = bufferedMutableSharedFlow(), sendsFlow: Flow> = bufferedMutableSharedFlow(), ) { @@ -2934,6 +2979,7 @@ class VaultRepositoryTest { coEvery { vaultDiskSource.getCollections(MOCK_USER_STATE.activeUserId) } returns collectionsFlow + coEvery { vaultDiskSource.getDomains(MOCK_USER_STATE.activeUserId) } returns domainsFlow coEvery { vaultDiskSource.getFolders(MOCK_USER_STATE.activeUserId) } returns foldersFlow coEvery { vaultDiskSource.getSends(MOCK_USER_STATE.activeUserId) } returns sendsFlow } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/model/DomainsDataUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/model/DomainsDataUtil.kt new file mode 100644 index 000000000..ab390ec64 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/model/DomainsDataUtil.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.vault.repository.model + +/** + * Create a mock [DomainsData] with a given [number]. + */ +fun createMockDomainsData(number: Int): DomainsData = + DomainsData( + equivalentDomains = listOf(listOf("mockEquivalentDomains-$number")), + globalEquivalentDomains = listOf( + DomainsData.GlobalEquivalentDomain( + isExcluded = false, + domains = listOf("domains-$number"), + type = number, + ), + ), + ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/DomainsExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/DomainsExtensionsTest.kt new file mode 100644 index 000000000..b75cda521 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/util/DomainsExtensionsTest.kt @@ -0,0 +1,71 @@ +package com.x8bit.bitwarden.data.vault.repository.util + +import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson +import com.x8bit.bitwarden.data.vault.repository.model.DomainsData +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class DomainsExtensionsTest { + @Test + fun `toDomainsData returns populated DomainsData when domains are non-null`() { + // Setup + val equivalentDomains = listOf(listOf(EQUIVALENT_DOMAIN)) + val globalDomains = listOf(GLOBAL_EQUIVALENT_DOMAIN) + val globalEquivalentDomain = SyncResponseJson.Domains.GlobalEquivalentDomain( + isExcluded = false, + domains = globalDomains, + type = 0, + ) + val domains = SyncResponseJson.Domains( + equivalentDomains = equivalentDomains, + globalEquivalentDomains = listOf(globalEquivalentDomain), + ) + val expectedGlobalEquivalentDomain = DomainsData.GlobalEquivalentDomain( + isExcluded = false, + domains = globalDomains, + type = 0, + ) + val expected = DomainsData( + equivalentDomains = equivalentDomains, + globalEquivalentDomains = listOf(expectedGlobalEquivalentDomain), + ) + + // Test + val actual = domains.toDomainsData() + + // Verify + assertEquals(expected, actual) + } + + @Test + fun `toDomainsData returns empty DomainsData when domains are null`() { + // Setup + val globalEquivalentDomain = SyncResponseJson.Domains.GlobalEquivalentDomain( + isExcluded = false, + domains = null, + type = 0, + ) + val domains = SyncResponseJson.Domains( + equivalentDomains = null, + globalEquivalentDomains = listOf(globalEquivalentDomain), + ) + val expectedGlobalEquivalentDomain = DomainsData.GlobalEquivalentDomain( + isExcluded = false, + domains = emptyList(), + type = 0, + ) + val expected = DomainsData( + equivalentDomains = emptyList(), + globalEquivalentDomains = listOf(expectedGlobalEquivalentDomain), + ) + + // Test + val actual = domains.toDomainsData() + + // Verify + assertEquals(expected, actual) + } +} + +private const val EQUIVALENT_DOMAIN: String = "EQUIVALENT_DOMAIN" +private const val GLOBAL_EQUIVALENT_DOMAIN: String = "GLOBAL_EQUIVALENT_DOMAIN"