mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-448: Create collections database table (#408)
This commit is contained in:
parent
ff4eeced33
commit
224371adc2
8 changed files with 246 additions and 1 deletions
|
@ -13,6 +13,11 @@ interface VaultDiskSource {
|
|||
*/
|
||||
fun getCiphers(userId: String): Flow<List<SyncResponseJson.Cipher>>
|
||||
|
||||
/**
|
||||
* Retrieves all collections from the data source for a given [userId].
|
||||
*/
|
||||
fun getCollections(userId: String): Flow<List<SyncResponseJson.Collection>>
|
||||
|
||||
/**
|
||||
* Retrieves all folders from the data source for a given [userId].
|
||||
*/
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.disk
|
||||
|
||||
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.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.network.model.SyncResponseJson
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -18,6 +20,7 @@ import kotlinx.serialization.json.Json
|
|||
*/
|
||||
class VaultDiskSourceImpl(
|
||||
private val ciphersDao: CiphersDao,
|
||||
private val collectionsDao: CollectionsDao,
|
||||
private val foldersDao: FoldersDao,
|
||||
private val json: Json,
|
||||
) : VaultDiskSource {
|
||||
|
@ -33,6 +36,24 @@ class VaultDiskSourceImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getCollections(
|
||||
userId: String,
|
||||
): Flow<List<SyncResponseJson.Collection>> =
|
||||
collectionsDao
|
||||
.getAllCollections(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
SyncResponseJson.Collection(
|
||||
id = entity.id,
|
||||
name = entity.name,
|
||||
organizationId = entity.organizationId,
|
||||
shouldHidePasswords = entity.shouldHidePasswords,
|
||||
externalId = entity.externalId,
|
||||
isReadOnly = entity.isReadOnly,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFolders(
|
||||
userId: String,
|
||||
): Flow<List<SyncResponseJson.Folder>> =
|
||||
|
@ -66,6 +87,22 @@ class VaultDiskSourceImpl(
|
|||
},
|
||||
)
|
||||
}
|
||||
val deferredCollections = async {
|
||||
collectionsDao.replaceAllCollections(
|
||||
userId = userId,
|
||||
collections = vault.collections.orEmpty().map { collection ->
|
||||
CollectionEntity(
|
||||
userId = userId,
|
||||
id = collection.id,
|
||||
name = collection.name,
|
||||
organizationId = collection.organizationId,
|
||||
shouldHidePasswords = collection.shouldHidePasswords,
|
||||
externalId = collection.externalId,
|
||||
isReadOnly = collection.isReadOnly,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
val deferredFolders = async {
|
||||
foldersDao.replaceAllFolders(
|
||||
userId = userId,
|
||||
|
@ -81,6 +118,7 @@ class VaultDiskSourceImpl(
|
|||
}
|
||||
awaitAll(
|
||||
deferredCiphers,
|
||||
deferredCollections,
|
||||
deferredFolders,
|
||||
)
|
||||
}
|
||||
|
@ -89,9 +127,11 @@ class VaultDiskSourceImpl(
|
|||
override suspend fun deleteVaultData(userId: String) {
|
||||
coroutineScope {
|
||||
val deferredCiphers = async { ciphersDao.deleteAllCiphers(userId = userId) }
|
||||
val deferredCollections = async { collectionsDao.deleteAllCollections(userId = userId) }
|
||||
val deferredFolders = async { foldersDao.deleteAllFolders(userId = userId) }
|
||||
awaitAll(
|
||||
deferredCiphers,
|
||||
deferredCollections,
|
||||
deferredFolders,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
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.CollectionEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Provides methods for inserting, retrieving, and deleting collections from the database using the
|
||||
* [CollectionEntity].
|
||||
*/
|
||||
@Dao
|
||||
interface CollectionsDao {
|
||||
|
||||
/**
|
||||
* Inserts multiple collections into the database.
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertCollections(collections: List<CollectionEntity>)
|
||||
|
||||
/**
|
||||
* Inserts a collection into the database.
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertCollection(collection: CollectionEntity)
|
||||
|
||||
/**
|
||||
* Retrieves all collections from the database for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM collections WHERE user_id = :userId")
|
||||
fun getAllCollections(userId: String): Flow<List<CollectionEntity>>
|
||||
|
||||
/**
|
||||
* Deletes all the stored collections associated with the given [userId].
|
||||
*/
|
||||
@Query("DELETE FROM collections WHERE user_id = :userId")
|
||||
suspend fun deleteAllCollections(userId: String)
|
||||
|
||||
/**
|
||||
* Deletes the stored collection associated with the given [userId] that matches the
|
||||
* [collectionId].
|
||||
*/
|
||||
@Query("DELETE FROM collections WHERE user_id = :userId AND id = :collectionId")
|
||||
suspend fun deleteCollection(userId: String, collectionId: String)
|
||||
|
||||
/**
|
||||
* Deletes all the stored [collections] associated with the given [userId] and then add all new
|
||||
* `collections` to the database.
|
||||
*/
|
||||
@Transaction
|
||||
suspend fun replaceAllCollections(userId: String, collections: List<CollectionEntity>) {
|
||||
deleteAllCollections(userId)
|
||||
insertCollections(collections)
|
||||
}
|
||||
}
|
|
@ -5,8 +5,10 @@ import androidx.room.RoomDatabase
|
|||
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.FoldersDao
|
||||
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
|
||||
|
||||
/**
|
||||
|
@ -15,6 +17,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity
|
|||
@Database(
|
||||
entities = [
|
||||
CipherEntity::class,
|
||||
CollectionEntity::class,
|
||||
FolderEntity::class,
|
||||
],
|
||||
version = 1,
|
||||
|
@ -27,6 +30,11 @@ abstract class VaultDatabase : RoomDatabase() {
|
|||
*/
|
||||
abstract fun cipherDao(): CiphersDao
|
||||
|
||||
/**
|
||||
* Provides the DAO for accessing collection data.
|
||||
*/
|
||||
abstract fun collectionDao(): CollectionsDao
|
||||
|
||||
/**
|
||||
* Provides the DAO for accessing folder data.
|
||||
*/
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
|||
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.FoldersDao
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.database.VaultDatabase
|
||||
import dagger.Module
|
||||
|
@ -38,6 +39,10 @@ class VaultDiskModule {
|
|||
@Singleton
|
||||
fun provideCipherDao(database: VaultDatabase): CiphersDao = database.cipherDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCollectionDao(database: VaultDatabase): CollectionsDao = database.collectionDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFolderDao(database: VaultDatabase): FoldersDao = database.folderDao()
|
||||
|
@ -46,10 +51,12 @@ class VaultDiskModule {
|
|||
@Singleton
|
||||
fun provideVaultDiskSource(
|
||||
ciphersDao: CiphersDao,
|
||||
collectionsDao: CollectionsDao,
|
||||
foldersDao: FoldersDao,
|
||||
json: Json,
|
||||
): VaultDiskSource = VaultDiskSourceImpl(
|
||||
ciphersDao = ciphersDao,
|
||||
collectionsDao = collectionsDao,
|
||||
foldersDao = foldersDao,
|
||||
json = json,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.disk.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* Entity representing a collection in the database.
|
||||
*/
|
||||
@Entity(tableName = "collections")
|
||||
data class CollectionEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "id")
|
||||
val id: String,
|
||||
|
||||
@ColumnInfo(name = "user_id", index = true)
|
||||
val userId: String,
|
||||
|
||||
@ColumnInfo(name = "organization_id")
|
||||
val organizationId: String,
|
||||
|
||||
@ColumnInfo(name = "should_hide_passwords")
|
||||
val shouldHidePasswords: Boolean,
|
||||
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
|
||||
@ColumnInfo(name = "external_id")
|
||||
val externalId: String?,
|
||||
|
||||
@ColumnInfo(name = "read_only")
|
||||
val isReadOnly: Boolean,
|
||||
)
|
|
@ -4,11 +4,14 @@ import app.cash.turbine.test
|
|||
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
|
||||
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.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.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 io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
@ -24,6 +27,7 @@ class VaultDiskSourceTest {
|
|||
|
||||
private val json = PlatformNetworkModule.providesJson()
|
||||
private lateinit var ciphersDao: FakeCiphersDao
|
||||
private lateinit var collectionsDao: FakeCollectionsDao
|
||||
private lateinit var foldersDao: FakeFoldersDao
|
||||
|
||||
private lateinit var vaultDiskSource: VaultDiskSource
|
||||
|
@ -31,9 +35,11 @@ class VaultDiskSourceTest {
|
|||
@BeforeEach
|
||||
fun setup() {
|
||||
ciphersDao = FakeCiphersDao()
|
||||
collectionsDao = FakeCollectionsDao()
|
||||
foldersDao = FakeFoldersDao()
|
||||
vaultDiskSource = VaultDiskSourceImpl(
|
||||
ciphersDao = ciphersDao,
|
||||
collectionsDao = collectionsDao,
|
||||
foldersDao = foldersDao,
|
||||
json = json,
|
||||
)
|
||||
|
@ -53,6 +59,20 @@ class VaultDiskSourceTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCollections should emit all CollectionsDao updates`() = runTest {
|
||||
val collectionEntities = listOf(COLLECTION_ENTITY)
|
||||
val collection = listOf(COLLECTION_1)
|
||||
|
||||
vaultDiskSource
|
||||
.getCollections(USER_ID)
|
||||
.test {
|
||||
assertEquals(emptyList<SyncResponseJson.Collection>(), awaitItem())
|
||||
collectionsDao.insertCollections(collectionEntities)
|
||||
assertEquals(collection, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFolders should emit all FoldersDao updates`() = runTest {
|
||||
val folderEntities = listOf(FOLDER_ENTITY)
|
||||
|
@ -70,6 +90,7 @@ class VaultDiskSourceTest {
|
|||
@Test
|
||||
fun `replaceVaultData should clear the daos and insert the new vault data`() = runTest {
|
||||
assertEquals(ciphersDao.storedCiphers, emptyList<CipherEntity>())
|
||||
assertEquals(collectionsDao.storedCollections, emptyList<CollectionEntity>())
|
||||
assertEquals(foldersDao.storedFolders, emptyList<FolderEntity>())
|
||||
|
||||
vaultDiskSource.replaceVaultData(USER_ID, VAULT_DATA)
|
||||
|
@ -84,6 +105,9 @@ class VaultDiskSourceTest {
|
|||
assertEquals(CIPHER_ENTITY.copy(cipherJson = ""), storedCipherEntity.copy(cipherJson = ""))
|
||||
assertJsonEquals(CIPHER_ENTITY.cipherJson, storedCipherEntity.cipherJson)
|
||||
|
||||
// Verify the collections dao is updated
|
||||
assertEquals(listOf(COLLECTION_ENTITY), collectionsDao.storedCollections)
|
||||
|
||||
// Verify the folders dao is updated
|
||||
assertEquals(listOf(FOLDER_ENTITY), foldersDao.storedFolders)
|
||||
}
|
||||
|
@ -91,9 +115,11 @@ class VaultDiskSourceTest {
|
|||
@Test
|
||||
fun `deleteVaultData should remove all vault data matching the user ID`() = runTest {
|
||||
assertFalse(ciphersDao.deleteCiphersCalled)
|
||||
assertFalse(collectionsDao.deleteCollectionsCalled)
|
||||
assertFalse(foldersDao.deleteFoldersCalled)
|
||||
vaultDiskSource.deleteVaultData(USER_ID)
|
||||
assertTrue(ciphersDao.deleteCiphersCalled)
|
||||
assertTrue(collectionsDao.deleteCollectionsCalled)
|
||||
assertTrue(foldersDao.deleteFoldersCalled)
|
||||
}
|
||||
}
|
||||
|
@ -101,11 +127,12 @@ class VaultDiskSourceTest {
|
|||
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 VAULT_DATA: SyncResponseJson = SyncResponseJson(
|
||||
folders = listOf(FOLDER_1),
|
||||
collections = null,
|
||||
collections = listOf(COLLECTION_1),
|
||||
profile = mockk<SyncResponseJson.Profile> {
|
||||
every { id } returns USER_ID
|
||||
},
|
||||
|
@ -217,6 +244,16 @@ private val CIPHER_ENTITY = CipherEntity(
|
|||
cipherJson = CIPHER_JSON,
|
||||
)
|
||||
|
||||
private val COLLECTION_ENTITY = CollectionEntity(
|
||||
id = "mockId-3",
|
||||
userId = USER_ID,
|
||||
organizationId = "mockOrganizationId-3",
|
||||
shouldHidePasswords = false,
|
||||
name = "mockName-3",
|
||||
externalId = "mockExternalId-3",
|
||||
isReadOnly = false,
|
||||
)
|
||||
|
||||
private val FOLDER_ENTITY = FolderEntity(
|
||||
id = "mockId-2",
|
||||
userId = USER_ID,
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.disk.dao
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CollectionEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class FakeCollectionsDao : CollectionsDao {
|
||||
|
||||
val storedCollections = mutableListOf<CollectionEntity>()
|
||||
|
||||
var deleteCollectionCalled: Boolean = false
|
||||
var deleteCollectionsCalled: Boolean = false
|
||||
|
||||
private val collectionsFlow = MutableSharedFlow<List<CollectionEntity>>(
|
||||
replay = 1,
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
|
||||
init {
|
||||
collectionsFlow.tryEmit(emptyList())
|
||||
}
|
||||
|
||||
override suspend fun deleteAllCollections(userId: String) {
|
||||
deleteCollectionsCalled = true
|
||||
storedCollections.removeAll { it.userId == userId }
|
||||
collectionsFlow.tryEmit(storedCollections.toList())
|
||||
}
|
||||
|
||||
override suspend fun deleteCollection(userId: String, collectionId: String) {
|
||||
deleteCollectionCalled = true
|
||||
storedCollections.removeAll { it.userId == userId && it.id == collectionId }
|
||||
collectionsFlow.tryEmit(storedCollections.toList())
|
||||
}
|
||||
|
||||
override fun getAllCollections(userId: String): Flow<List<CollectionEntity>> =
|
||||
collectionsFlow.map { ciphers -> ciphers.filter { it.userId == userId } }
|
||||
|
||||
override suspend fun insertCollections(collections: List<CollectionEntity>) {
|
||||
storedCollections.addAll(collections)
|
||||
collectionsFlow.tryEmit(storedCollections.toList())
|
||||
}
|
||||
|
||||
override suspend fun insertCollection(collection: CollectionEntity) {
|
||||
storedCollections.add(collection)
|
||||
collectionsFlow.tryEmit(storedCollections.toList())
|
||||
}
|
||||
|
||||
override suspend fun replaceAllCollections(
|
||||
userId: String,
|
||||
collections: List<CollectionEntity>,
|
||||
) {
|
||||
storedCollections.removeAll { it.userId == userId }
|
||||
storedCollections.addAll(collections)
|
||||
collectionsFlow.tryEmit(storedCollections.toList())
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue