Add Domains database (#784)

This commit is contained in:
Lucas Kivi 2024-01-25 19:17:03 -06:00 committed by Álison Fernandes
parent 5fa49c8b53
commit 52acc2fa47
15 changed files with 416 additions and 5 deletions

View file

@ -34,6 +34,11 @@ interface VaultDiskSource {
*/
fun getCollections(userId: String): Flow<List<SyncResponseJson.Collection>>
/**
* Retrieves all domains from the data source for a given [userId].
*/
fun getDomains(userId: String): Flow<SyncResponseJson.Domains>
/**
* Saves a folder to the data source for the given [userId].
*/

View file

@ -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<SyncResponseJson.Domains> =
domainsDao
.getDomains(userId)
.map { entity ->
json.decodeFromString<SyncResponseJson.Domains>(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,
)

View file

@ -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<DomainsEntity>
/**
* Inserts domains into the database.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDomains(domains: DomainsEntity)
}

View file

@ -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.
*/

View file

@ -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,

View file

@ -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,
)

View file

@ -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<DataState<List<CollectionView>>>
/**
* 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<DataState<DomainsData>>
/**
* Flow that represents all folders for the active user.
*

View file

@ -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<List<CollectionView>>>(DataState.Loading)
private val mutableDomainsStateFlow =
MutableStateFlow<DataState<DomainsData>>(DataState.Loading)
override var vaultFilterType: VaultFilterType = VaultFilterType.AllVaults
override val vaultDataStateFlow: StateFlow<DataState<VaultData>> =
@ -171,6 +176,9 @@ class VaultRepositoryImpl(
override val ciphersStateFlow: StateFlow<DataState<List<CipherView>>>
get() = mutableCiphersStateFlow.asStateFlow()
override val domainsStateFlow: StateFlow<DataState<DomainsData>>
get() = mutableDomainsStateFlow.asStateFlow()
override val foldersStateFlow: StateFlow<DataState<List<FolderView>>>
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<DataState<DomainsData>> =
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<DataState<List<FolderView>>> =

View file

@ -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<List<String>>,
val globalEquivalentDomains: List<GlobalEquivalentDomain>,
) {
/**
* 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<String>,
val type: Int,
)
}

View file

@ -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,
)

View file

@ -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<CipherEntity>())
assertEquals(collectionsDao.storedCollections, emptyList<CollectionEntity>())
assertNull(domainsDao.storedDomains)
assertEquals(foldersDao.storedFolders, emptyList<FolderEntity>())
assertEquals(sendsDao.storedSends, emptyList<SendEntity>())
@ -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,

View file

@ -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<DomainsEntity?>()
override suspend fun deleteDomains(userId: String) {
deleteDomainsCalled = true
}
override fun getDomains(userId: String): Flow<DomainsEntity> {
getDomainsCalled = true
return mutableDomainsFlow.filterNotNull()
}
override suspend fun insertDomains(domains: DomainsEntity) {
insertDomainsCalled = true
storedDomains = domains
mutableDomainsFlow.tryEmit(domains)
}
}

View file

@ -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<List<CollectionView>>(mockException),
vaultRepository.collectionsStateFlow.value,
)
assertEquals(
DataState.Error<DomainsData>(mockException),
vaultRepository.domainsStateFlow.value,
)
assertEquals(
DataState.Error<List<FolderView>>(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<SyncResponseJson.Domains>()
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<List<SyncResponseJson.Cipher>> = bufferedMutableSharedFlow(),
collectionsFlow: Flow<List<SyncResponseJson.Collection>> = bufferedMutableSharedFlow(),
domainsFlow: Flow<SyncResponseJson.Domains> = bufferedMutableSharedFlow(),
foldersFlow: Flow<List<SyncResponseJson.Folder>> = bufferedMutableSharedFlow(),
sendsFlow: Flow<List<SyncResponseJson.Send>> = 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
}

View file

@ -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,
),
),
)

View file

@ -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"