mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 22:52:20 -06:00
BIT-431: Add a table to the vault database for folders (#403)
This commit is contained in:
parent
34101245dd
commit
34e3fbcc04
@ -161,8 +161,8 @@ koverReport {
|
|||||||
// Empty Composables
|
// Empty Composables
|
||||||
"com.x8bit.bitwarden.ui.platform.feature.splash.SplashScreenKt",
|
"com.x8bit.bitwarden.ui.platform.feature.splash.SplashScreenKt",
|
||||||
// Databases
|
// Databases
|
||||||
"*.database.*Database",
|
"*.database.*Database*",
|
||||||
"*.dao.*Dao",
|
"*.dao.*Dao*",
|
||||||
)
|
)
|
||||||
packages(
|
packages(
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
|
|||||||
@ -13,6 +13,11 @@ interface VaultDiskSource {
|
|||||||
*/
|
*/
|
||||||
fun getCiphers(userId: String): Flow<List<SyncResponseJson.Cipher>>
|
fun getCiphers(userId: String): Flow<List<SyncResponseJson.Cipher>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all folders from the data source for a given [userId].
|
||||||
|
*/
|
||||||
|
fun getFolders(userId: String): Flow<List<SyncResponseJson.Folder>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces all [vault] data for a given [userId] with the new `vault`.
|
* Replaces all [vault] data for a given [userId] with the new `vault`.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
package com.x8bit.bitwarden.data.vault.datasource.disk
|
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.CiphersDao
|
||||||
|
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.CipherEntity
|
||||||
|
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.SyncResponseJson
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@ -13,6 +18,7 @@ import kotlinx.serialization.json.Json
|
|||||||
*/
|
*/
|
||||||
class VaultDiskSourceImpl(
|
class VaultDiskSourceImpl(
|
||||||
private val ciphersDao: CiphersDao,
|
private val ciphersDao: CiphersDao,
|
||||||
|
private val foldersDao: FoldersDao,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
) : VaultDiskSource {
|
) : VaultDiskSource {
|
||||||
|
|
||||||
@ -27,21 +33,67 @@ class VaultDiskSourceImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun replaceVaultData(userId: String, vault: SyncResponseJson) {
|
override fun getFolders(
|
||||||
ciphersDao.replaceAllCiphers(
|
userId: String,
|
||||||
userId = userId,
|
): Flow<List<SyncResponseJson.Folder>> =
|
||||||
ciphers = vault.ciphers.orEmpty().map { cipher ->
|
foldersDao
|
||||||
CipherEntity(
|
.getAllFolders(userId = userId)
|
||||||
id = cipher.id,
|
.map { entities ->
|
||||||
|
entities.map { entity ->
|
||||||
|
SyncResponseJson.Folder(
|
||||||
|
id = entity.id,
|
||||||
|
name = entity.name,
|
||||||
|
revisionDate = entity.revisionDate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun replaceVaultData(
|
||||||
|
userId: String,
|
||||||
|
vault: SyncResponseJson,
|
||||||
|
) {
|
||||||
|
coroutineScope {
|
||||||
|
val deferredCiphers = async {
|
||||||
|
ciphersDao.replaceAllCiphers(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
cipherType = json.encodeToString(cipher.type),
|
ciphers = vault.ciphers.orEmpty().map { cipher ->
|
||||||
cipherJson = json.encodeToString(cipher),
|
CipherEntity(
|
||||||
|
id = cipher.id,
|
||||||
|
userId = userId,
|
||||||
|
cipherType = json.encodeToString(cipher.type),
|
||||||
|
cipherJson = json.encodeToString(cipher),
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
val deferredFolders = async {
|
||||||
|
foldersDao.replaceAllFolders(
|
||||||
|
userId = userId,
|
||||||
|
folders = vault.folders.orEmpty().map { folder ->
|
||||||
|
FolderEntity(
|
||||||
|
userId = userId,
|
||||||
|
id = folder.id,
|
||||||
|
name = folder.name,
|
||||||
|
revisionDate = folder.revisionDate,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitAll(
|
||||||
|
deferredCiphers,
|
||||||
|
deferredFolders,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteVaultData(userId: String) {
|
override suspend fun deleteVaultData(userId: String) {
|
||||||
ciphersDao.deleteAllCiphers(userId = userId)
|
coroutineScope {
|
||||||
|
val deferredCiphers = async { ciphersDao.deleteAllCiphers(userId = userId) }
|
||||||
|
val deferredFolders = async { foldersDao.deleteAllFolders(userId = userId) }
|
||||||
|
awaitAll(
|
||||||
|
deferredCiphers,
|
||||||
|
deferredFolders,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
package com.x8bit.bitwarden.data.vault.datasource.disk.convertor
|
||||||
|
|
||||||
|
import androidx.room.ProvidedTypeConverter
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TypeConverter] to convert a [ZonedDateTime] to and from a [Long].
|
||||||
|
*/
|
||||||
|
@ProvidedTypeConverter
|
||||||
|
object ZonedDateTimeTypeConverter {
|
||||||
|
/**
|
||||||
|
* A [TypeConverter] to convert a [Long] to a [ZonedDateTime].
|
||||||
|
*/
|
||||||
|
@TypeConverter
|
||||||
|
fun fromTimestamp(
|
||||||
|
value: Long?,
|
||||||
|
): ZonedDateTime? = value?.let {
|
||||||
|
ZonedDateTime.ofInstant(Instant.ofEpochSecond(it), ZoneOffset.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TypeConverter] to convert a [ZonedDateTime] to a [Long].
|
||||||
|
*/
|
||||||
|
@TypeConverter
|
||||||
|
fun toTimestamp(
|
||||||
|
localDateTime: ZonedDateTime?,
|
||||||
|
): Long? = localDateTime?.toEpochSecond()
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
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.FolderEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods for inserting, retrieving, and deleting folders from the database using the
|
||||||
|
* [FolderEntity].
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface FoldersDao {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts multiple folders into the database.
|
||||||
|
*/
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertFolders(folders: List<FolderEntity>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a folder into the database.
|
||||||
|
*/
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertFolder(folder: FolderEntity)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all folders from the database for a given [userId].
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM folders WHERE user_id = :userId")
|
||||||
|
fun getAllFolders(
|
||||||
|
userId: String,
|
||||||
|
): Flow<List<FolderEntity>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all the stored folders associated with the given [userId].
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM folders WHERE user_id = :userId")
|
||||||
|
suspend fun deleteAllFolders(userId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the stored folder associated with the given [userId] that matches the [folderId].
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM folders WHERE user_id = :userId AND id = :folderId")
|
||||||
|
suspend fun deleteFolder(userId: String, folderId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all the stored [folders] associated with the given [userId] and then add all new
|
||||||
|
* `folders` to the database.
|
||||||
|
*/
|
||||||
|
@Transaction
|
||||||
|
suspend fun replaceAllFolders(userId: String, folders: List<FolderEntity>) {
|
||||||
|
deleteAllFolders(userId)
|
||||||
|
insertFolders(folders)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,8 +2,12 @@ package com.x8bit.bitwarden.data.vault.datasource.disk.database
|
|||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
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.CiphersDao
|
||||||
|
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.CipherEntity
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Room database for storing any persisted data from the vault sync.
|
* Room database for storing any persisted data from the vault sync.
|
||||||
@ -11,13 +15,20 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CipherEntity
|
|||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
CipherEntity::class,
|
CipherEntity::class,
|
||||||
|
FolderEntity::class,
|
||||||
],
|
],
|
||||||
version = 1,
|
version = 1,
|
||||||
)
|
)
|
||||||
|
@TypeConverters(ZonedDateTimeTypeConverter::class)
|
||||||
abstract class VaultDatabase : RoomDatabase() {
|
abstract class VaultDatabase : RoomDatabase() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the DAO for accessing cipher data.
|
* Provides the DAO for accessing cipher data.
|
||||||
*/
|
*/
|
||||||
abstract fun cipherDao(): CiphersDao
|
abstract fun cipherDao(): CiphersDao
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the DAO for accessing folder data.
|
||||||
|
*/
|
||||||
|
abstract fun folderDao(): FoldersDao
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import android.app.Application
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
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.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.CiphersDao
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.FoldersDao
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.disk.database.VaultDatabase
|
import com.x8bit.bitwarden.data.vault.datasource.disk.database.VaultDatabase
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@ -29,19 +31,26 @@ class VaultDiskModule {
|
|||||||
klass = VaultDatabase::class.java,
|
klass = VaultDatabase::class.java,
|
||||||
name = "vault_database",
|
name = "vault_database",
|
||||||
)
|
)
|
||||||
|
.addTypeConverter(ZonedDateTimeTypeConverter)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideCipherDao(database: VaultDatabase): CiphersDao = database.cipherDao()
|
fun provideCipherDao(database: VaultDatabase): CiphersDao = database.cipherDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideFolderDao(database: VaultDatabase): FoldersDao = database.folderDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideVaultDiskSource(
|
fun provideVaultDiskSource(
|
||||||
ciphersDao: CiphersDao,
|
ciphersDao: CiphersDao,
|
||||||
|
foldersDao: FoldersDao,
|
||||||
json: Json,
|
json: Json,
|
||||||
): VaultDiskSource = VaultDiskSourceImpl(
|
): VaultDiskSource = VaultDiskSourceImpl(
|
||||||
ciphersDao = ciphersDao,
|
ciphersDao = ciphersDao,
|
||||||
|
foldersDao = foldersDao,
|
||||||
json = json,
|
json = json,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.x8bit.bitwarden.data.vault.datasource.disk.entity
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity representing a folder in the database.
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "folders")
|
||||||
|
data class FolderEntity(
|
||||||
|
@PrimaryKey(autoGenerate = false)
|
||||||
|
@ColumnInfo(name = "id")
|
||||||
|
val id: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "user_id", index = true)
|
||||||
|
val userId: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "name")
|
||||||
|
val name: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "revision_date")
|
||||||
|
val revisionDate: ZonedDateTime,
|
||||||
|
)
|
||||||
@ -4,9 +4,12 @@ import app.cash.turbine.test
|
|||||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
|
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
|
||||||
import com.x8bit.bitwarden.data.util.assertJsonEquals
|
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.FakeCiphersDao
|
||||||
|
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.CipherEntity
|
||||||
|
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.SyncResponseJson
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@ -15,25 +18,29 @@ import org.junit.jupiter.api.Assertions.assertFalse
|
|||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
class VaultDiskSourceTest {
|
class VaultDiskSourceTest {
|
||||||
|
|
||||||
private val json = PlatformNetworkModule.providesJson()
|
private val json = PlatformNetworkModule.providesJson()
|
||||||
private lateinit var ciphersDao: FakeCiphersDao
|
private lateinit var ciphersDao: FakeCiphersDao
|
||||||
|
private lateinit var foldersDao: FakeFoldersDao
|
||||||
|
|
||||||
private lateinit var vaultDiskSource: VaultDiskSource
|
private lateinit var vaultDiskSource: VaultDiskSource
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setup() {
|
fun setup() {
|
||||||
ciphersDao = FakeCiphersDao()
|
ciphersDao = FakeCiphersDao()
|
||||||
|
foldersDao = FakeFoldersDao()
|
||||||
vaultDiskSource = VaultDiskSourceImpl(
|
vaultDiskSource = VaultDiskSourceImpl(
|
||||||
ciphersDao = ciphersDao,
|
ciphersDao = ciphersDao,
|
||||||
|
foldersDao = foldersDao,
|
||||||
json = json,
|
json = json,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getCiphers should emit all dao updates`() = runTest {
|
fun `getCiphers should emit all CiphersDao updates`() = runTest {
|
||||||
val cipherEntities = listOf(CIPHER_ENTITY)
|
val cipherEntities = listOf(CIPHER_ENTITY)
|
||||||
val ciphers = listOf(CIPHER_1)
|
val ciphers = listOf(CIPHER_1)
|
||||||
|
|
||||||
@ -47,33 +54,57 @@ class VaultDiskSourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `replaceVaultData should clear the dao and insert the encoded ciphers`() = runTest {
|
fun `getFolders should emit all FoldersDao updates`() = runTest {
|
||||||
|
val folderEntities = listOf(FOLDER_ENTITY)
|
||||||
|
val folders = listOf(FOLDER_1)
|
||||||
|
|
||||||
|
vaultDiskSource
|
||||||
|
.getFolders(USER_ID)
|
||||||
|
.test {
|
||||||
|
assertEquals(emptyList<SyncResponseJson.Folder>(), awaitItem())
|
||||||
|
foldersDao.insertFolders(folderEntities)
|
||||||
|
assertEquals(folders, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `replaceVaultData should clear the daos and insert the new vault data`() = runTest {
|
||||||
assertEquals(ciphersDao.storedCiphers, emptyList<CipherEntity>())
|
assertEquals(ciphersDao.storedCiphers, emptyList<CipherEntity>())
|
||||||
|
assertEquals(foldersDao.storedFolders, emptyList<FolderEntity>())
|
||||||
|
|
||||||
vaultDiskSource.replaceVaultData(USER_ID, VAULT_DATA)
|
vaultDiskSource.replaceVaultData(USER_ID, VAULT_DATA)
|
||||||
|
|
||||||
assertEquals(1, ciphersDao.storedCiphers.size)
|
assertEquals(1, ciphersDao.storedCiphers.size)
|
||||||
val storedEntity = ciphersDao.storedCiphers.first()
|
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
|
// We cannot compare the JSON strings directly because of formatting differences
|
||||||
// So we split that off into its own assertion.
|
// So we split that off into its own assertion.
|
||||||
assertEquals(CIPHER_ENTITY.copy(cipherJson = ""), storedEntity.copy(cipherJson = ""))
|
assertEquals(CIPHER_ENTITY.copy(cipherJson = ""), storedCipherEntity.copy(cipherJson = ""))
|
||||||
assertJsonEquals(CIPHER_ENTITY.cipherJson, storedEntity.cipherJson)
|
assertJsonEquals(CIPHER_ENTITY.cipherJson, storedCipherEntity.cipherJson)
|
||||||
|
|
||||||
|
// Verify the folders dao is updated
|
||||||
|
assertEquals(listOf(FOLDER_ENTITY), foldersDao.storedFolders)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `deleteVaultData should remove all ciphers matching the user ID`() = runTest {
|
fun `deleteVaultData should remove all vault data matching the user ID`() = runTest {
|
||||||
assertFalse(ciphersDao.deleteCiphersCalled)
|
assertFalse(ciphersDao.deleteCiphersCalled)
|
||||||
|
assertFalse(foldersDao.deleteFoldersCalled)
|
||||||
vaultDiskSource.deleteVaultData(USER_ID)
|
vaultDiskSource.deleteVaultData(USER_ID)
|
||||||
assertTrue(ciphersDao.deleteCiphersCalled)
|
assertTrue(ciphersDao.deleteCiphersCalled)
|
||||||
|
assertTrue(foldersDao.deleteFoldersCalled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val USER_ID: String = "test_user_id"
|
private const val USER_ID: String = "test_user_id"
|
||||||
|
|
||||||
private val CIPHER_1: SyncResponseJson.Cipher = createMockCipher(1)
|
private val CIPHER_1: SyncResponseJson.Cipher = createMockCipher(1)
|
||||||
|
private val FOLDER_1: SyncResponseJson.Folder = createMockFolder(2)
|
||||||
|
|
||||||
private val VAULT_DATA: SyncResponseJson = SyncResponseJson(
|
private val VAULT_DATA: SyncResponseJson = SyncResponseJson(
|
||||||
folders = null,
|
folders = listOf(FOLDER_1),
|
||||||
collections = null,
|
collections = null,
|
||||||
profile = mockk<SyncResponseJson.Profile> {
|
profile = mockk<SyncResponseJson.Profile> {
|
||||||
every { id } returns USER_ID
|
every { id } returns USER_ID
|
||||||
@ -185,3 +216,10 @@ private val CIPHER_ENTITY = CipherEntity(
|
|||||||
cipherType = "1",
|
cipherType = "1",
|
||||||
cipherJson = CIPHER_JSON,
|
cipherJson = CIPHER_JSON,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val FOLDER_ENTITY = FolderEntity(
|
||||||
|
id = "mockId-2",
|
||||||
|
userId = USER_ID,
|
||||||
|
name = "mockName-2",
|
||||||
|
revisionDate = ZonedDateTime.parse("2023-10-27T12:00Z"),
|
||||||
|
)
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.x8bit.bitwarden.data.vault.datasource.disk.convertor
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
class ZonedDateTimeTypeConverterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromTimestamp should return null when value is null`() {
|
||||||
|
val value: Long? = null
|
||||||
|
|
||||||
|
val result = ZonedDateTimeTypeConverter.fromTimestamp(value)
|
||||||
|
|
||||||
|
assertNull(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromTimestamp should return correct ZonedDateTime when value is not null`() {
|
||||||
|
val expected = ZonedDateTime.parse("2023-12-15T20:38:06Z")
|
||||||
|
val value = expected.toEpochSecond()
|
||||||
|
|
||||||
|
val result = ZonedDateTimeTypeConverter.fromTimestamp(value)
|
||||||
|
|
||||||
|
assertEquals(expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toTimestamp should return null when value is null`() {
|
||||||
|
val value: ZonedDateTime? = null
|
||||||
|
|
||||||
|
val result = ZonedDateTimeTypeConverter.toTimestamp(value)
|
||||||
|
|
||||||
|
assertNull(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toTimestamp should return correct Long when value is not null`() {
|
||||||
|
val value = ZonedDateTime.parse("2023-12-15T20:38:06Z")
|
||||||
|
val expected = value.toEpochSecond()
|
||||||
|
|
||||||
|
val result = ZonedDateTimeTypeConverter.toTimestamp(value)
|
||||||
|
|
||||||
|
assertEquals(expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.x8bit.bitwarden.data.vault.datasource.disk.dao
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class FakeFoldersDao : FoldersDao {
|
||||||
|
|
||||||
|
val storedFolders = mutableListOf<FolderEntity>()
|
||||||
|
|
||||||
|
var deleteFolderCalled: Boolean = false
|
||||||
|
var deleteFoldersCalled: Boolean = false
|
||||||
|
|
||||||
|
private val foldersFlow = MutableSharedFlow<List<FolderEntity>>(
|
||||||
|
replay = 1,
|
||||||
|
extraBufferCapacity = Int.MAX_VALUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
foldersFlow.tryEmit(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteAllFolders(userId: String) {
|
||||||
|
deleteFoldersCalled = true
|
||||||
|
storedFolders.removeAll { it.userId == userId }
|
||||||
|
foldersFlow.tryEmit(storedFolders.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteFolder(userId: String, folderId: String) {
|
||||||
|
deleteFolderCalled = true
|
||||||
|
storedFolders.removeAll { it.userId == userId && it.id == folderId }
|
||||||
|
foldersFlow.tryEmit(storedFolders.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllFolders(userId: String): Flow<List<FolderEntity>> =
|
||||||
|
foldersFlow.map { ciphers -> ciphers.filter { it.userId == userId } }
|
||||||
|
|
||||||
|
override suspend fun insertFolders(folders: List<FolderEntity>) {
|
||||||
|
storedFolders.addAll(folders)
|
||||||
|
foldersFlow.tryEmit(storedFolders.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun insertFolder(folder: FolderEntity) {
|
||||||
|
storedFolders.add(folder)
|
||||||
|
foldersFlow.tryEmit(storedFolders.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun replaceAllFolders(userId: String, folders: List<FolderEntity>) {
|
||||||
|
storedFolders.removeAll { it.userId == userId }
|
||||||
|
storedFolders.addAll(folders)
|
||||||
|
foldersFlow.tryEmit(storedFolders.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user