mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 19:28:44 +03:00
BIT-205: Populate vault with login items (#246)
This commit is contained in:
parent
3e6ce662d8
commit
fd10de9456
13 changed files with 688 additions and 90 deletions
|
@ -8,31 +8,24 @@ import kotlinx.serialization.encoding.Decoder
|
|||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
|
||||
/**
|
||||
* Used to serialize and deserialize [LocalDateTime].
|
||||
*/
|
||||
class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
private val localDateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SS'Z'")
|
||||
private val localDateTimeFormatterNanoSeconds =
|
||||
private val dateTimeFormatterDeserialization = DateTimeFormatter
|
||||
.ofPattern("yyyy-MM-dd'T'HH:mm:ss.[SSSSSSS][SSSSSS][SSSSS][SSSS][SSS][SS][S]'Z'")
|
||||
private val dateTimeFormatterSerialization =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'")
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor(serialName = "LocalDateTime", kind = PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime =
|
||||
decoder.decodeString().let { dateString ->
|
||||
try {
|
||||
LocalDateTime
|
||||
.parse(dateString, localDateTimeFormatter)
|
||||
} catch (exception: DateTimeParseException) {
|
||||
LocalDateTime
|
||||
.parse(dateString, localDateTimeFormatterNanoSeconds)
|
||||
}
|
||||
LocalDateTime.parse(dateString, dateTimeFormatterDeserialization)
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: LocalDateTime) {
|
||||
encoder.encodeString(localDateTimeFormatter.format(value))
|
||||
encoder.encodeString(dateTimeFormatterSerialization.format(value))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,12 @@ interface VaultSdkSource {
|
|||
/**
|
||||
* Decrypts a list of [Cipher]s returning a list of [CipherListView] wrapped in a [Result].
|
||||
*/
|
||||
suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherListView>>
|
||||
suspend fun decryptCipherListCollection(cipherList: List<Cipher>): Result<List<CipherListView>>
|
||||
|
||||
/**
|
||||
* Decrypts a list of [Cipher]s returning a list of [CipherView] wrapped in a [Result].
|
||||
*/
|
||||
suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherView>>
|
||||
|
||||
/**
|
||||
* Decrypts a [Folder] returning a [FolderView] wrapped in a [Result].
|
||||
|
|
|
@ -35,9 +35,14 @@ class VaultSdkSourceImpl(
|
|||
override suspend fun decryptCipher(cipher: Cipher): Result<CipherView> =
|
||||
runCatching { clientVault.ciphers().decrypt(cipher) }
|
||||
|
||||
override suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherListView>> =
|
||||
override suspend fun decryptCipherListCollection(
|
||||
cipherList: List<Cipher>,
|
||||
): Result<List<CipherListView>> =
|
||||
runCatching { clientVault.ciphers().decryptList(cipherList) }
|
||||
|
||||
override suspend fun decryptCipherList(cipherList: List<Cipher>): Result<List<CipherView>> =
|
||||
runCatching { cipherList.map { clientVault.ciphers().decrypt(it) } }
|
||||
|
||||
override suspend fun decryptFolder(folder: Folder): Result<FolderView> =
|
||||
runCatching { clientVault.folders().decrypt(folder) }
|
||||
|
||||
|
|
|
@ -20,6 +20,10 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -37,6 +41,8 @@ class VaultRepositoryImpl constructor(
|
|||
|
||||
private var syncJob: Job = Job().apply { complete() }
|
||||
|
||||
private var willSyncAfterUnlock = false
|
||||
|
||||
private val vaultDataMutableStateFlow =
|
||||
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
|
||||
|
||||
|
@ -48,7 +54,7 @@ class VaultRepositoryImpl constructor(
|
|||
}
|
||||
|
||||
override fun sync() {
|
||||
if (!syncJob.isCompleted) return
|
||||
if (!syncJob.isCompleted || willSyncAfterUnlock) return
|
||||
vaultDataMutableStateFlow.value.data?.let { data ->
|
||||
vaultDataMutableStateFlow.update {
|
||||
DataState.Pending(data = data)
|
||||
|
@ -86,10 +92,16 @@ class VaultRepositoryImpl constructor(
|
|||
}
|
||||
|
||||
override suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult {
|
||||
return initializeCrypto(masterPassword = masterPassword)
|
||||
.also { vaultUnlockedResult ->
|
||||
if (vaultUnlockedResult is VaultUnlockResult.Success) sync()
|
||||
return flow {
|
||||
willSyncAfterUnlock = true
|
||||
emit(initializeCrypto(masterPassword = masterPassword))
|
||||
}
|
||||
.onEach {
|
||||
willSyncAfterUnlock = false
|
||||
if (it is VaultUnlockResult.Success) sync()
|
||||
}
|
||||
.onCompletion { willSyncAfterUnlock = false }
|
||||
.first()
|
||||
}
|
||||
|
||||
private fun storeUserKeyAndPrivateKey(
|
||||
|
@ -156,7 +168,7 @@ class VaultRepositoryImpl constructor(
|
|||
onSuccess = { (decryptedCipherList, decryptedFolderList) ->
|
||||
DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherListViewList = decryptedCipherList,
|
||||
cipherViewList = decryptedCipherList,
|
||||
folderViewList = decryptedFolderList,
|
||||
),
|
||||
)
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package com.x8bit.bitwarden.data.vault.repository.model
|
||||
|
||||
import com.bitwarden.core.CipherListView
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.FolderView
|
||||
|
||||
/**
|
||||
* Represents decrypted vault data.
|
||||
*
|
||||
* @param cipherListViewList List of decrypted ciphers.
|
||||
* @param cipherViewList List of decrypted ciphers.
|
||||
* @param folderViewList List of decrypted folders.
|
||||
*/
|
||||
data class VaultData(
|
||||
val cipherListViewList: List<CipherListView>,
|
||||
val cipherViewList: List<CipherView>,
|
||||
val folderViewList: List<FolderView>,
|
||||
)
|
||||
|
|
|
@ -7,12 +7,16 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AccountSummary
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toViewState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -32,8 +36,8 @@ private const val KEY_STATE = "state"
|
|||
@HiltViewModel
|
||||
class VaultViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
|
||||
// TODO retrieve this from the data layer BIT-205
|
||||
initialState = savedStateHandle[KEY_STATE] ?: VaultState(
|
||||
initials = activeAccountSummary.initials,
|
||||
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||
|
@ -46,30 +50,18 @@ class VaultViewModel @Inject constructor(
|
|||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
vaultRepository
|
||||
.vaultDataStateFlow
|
||||
.onEach { sendAction(VaultAction.Internal.VaultDataReceive(vaultData = it)) }
|
||||
.launchIn(viewModelScope)
|
||||
// TODO remove this block once vault unlocked is implemented in BIT-1082
|
||||
viewModelScope.launch {
|
||||
// TODO will need to load actual vault items BIT-205
|
||||
@Suppress("MagicNumber")
|
||||
delay(2000)
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = VaultState.ViewState.Content(
|
||||
loginItemsCount = 0,
|
||||
cardItemsCount = 0,
|
||||
identityItemsCount = 0,
|
||||
secureNoteItemsCount = 0,
|
||||
favoriteItems = emptyList(),
|
||||
folderItems = listOf(
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = null,
|
||||
name = R.string.folder_none.asText(),
|
||||
itemCount = 0,
|
||||
),
|
||||
),
|
||||
// TODO Take into account the max threshold of no folder as well as the
|
||||
// case where it is empty, in which case, no folder is a folder. BIT-205
|
||||
noFolderItems = emptyList(),
|
||||
trashItemsCount = 0,
|
||||
delay(5000)
|
||||
if (vaultRepository.vaultDataStateFlow.value == DataState.Loading) {
|
||||
sendAction(
|
||||
VaultAction.Internal.VaultDataReceive(
|
||||
DataState.Error(error = IllegalStateException()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -78,17 +70,18 @@ class VaultViewModel @Inject constructor(
|
|||
|
||||
override fun handleAction(action: VaultAction) {
|
||||
when (action) {
|
||||
VaultAction.AddItemClick -> handleAddItemClick()
|
||||
VaultAction.CardGroupClick -> handleCardClick()
|
||||
is VaultAction.AddItemClick -> handleAddItemClick()
|
||||
is VaultAction.CardGroupClick -> handleCardClick()
|
||||
is VaultAction.FolderClick -> handleFolderItemClick(action)
|
||||
VaultAction.IdentityGroupClick -> handleIdentityClick()
|
||||
VaultAction.LoginGroupClick -> handleLoginClick()
|
||||
VaultAction.SearchIconClick -> handleSearchIconClick()
|
||||
is VaultAction.IdentityGroupClick -> handleIdentityClick()
|
||||
is VaultAction.LoginGroupClick -> handleLoginClick()
|
||||
is VaultAction.SearchIconClick -> handleSearchIconClick()
|
||||
is VaultAction.AccountSwitchClick -> handleAccountSwitchClick(action)
|
||||
VaultAction.AddAccountClick -> handleAddAccountClick()
|
||||
VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
||||
VaultAction.TrashClick -> handleTrashClick()
|
||||
is VaultAction.AddAccountClick -> handleAddAccountClick()
|
||||
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
||||
is VaultAction.TrashClick -> handleTrashClick()
|
||||
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
|
||||
is VaultAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,6 +143,41 @@ class VaultViewModel @Inject constructor(
|
|||
private fun handleVaultItemClick(action: VaultAction.VaultItemClick) {
|
||||
sendEvent(VaultEvent.NavigateToVaultItem(action.vaultItem.id))
|
||||
}
|
||||
|
||||
private fun handleVaultDataReceive(action: VaultAction.Internal.VaultDataReceive) {
|
||||
when (val vaultData = action.vaultData) {
|
||||
is DataState.Error -> vaultErrorReceive(vaultData = vaultData)
|
||||
is DataState.Loaded -> vaultLoadedReceive(vaultData = vaultData)
|
||||
is DataState.Loading -> vaultLoadingReceive()
|
||||
is DataState.NoNetwork -> vaultNoNetworkReceive(vaultData = vaultData)
|
||||
is DataState.Pending -> vaultPendingReceive(vaultData = vaultData)
|
||||
}
|
||||
}
|
||||
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
|
||||
// TODO update state to error state BIT-1157
|
||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
|
||||
sendEvent(VaultEvent.ShowToast(message = "Vault error state not yet implemented"))
|
||||
}
|
||||
|
||||
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
|
||||
mutableStateFlow.update { it.copy(viewState = vaultData.data.toViewState()) }
|
||||
}
|
||||
|
||||
private fun vaultLoadingReceive() {
|
||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.Loading) }
|
||||
}
|
||||
|
||||
private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
|
||||
// TODO update state to no network state BIT-1158
|
||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
|
||||
sendEvent(VaultEvent.ShowToast(message = "Vault no network state not yet implemented"))
|
||||
}
|
||||
|
||||
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
|
||||
// TODO update state to refresh state BIT-505
|
||||
mutableStateFlow.update { it.copy(viewState = vaultData.data.toViewState()) }
|
||||
sendEvent(VaultEvent.ShowToast(message = "Refreshing"))
|
||||
}
|
||||
//endregion VaultAction Handlers
|
||||
}
|
||||
|
||||
|
@ -499,4 +527,17 @@ sealed class VaultAction {
|
|||
* User clicked the trash button.
|
||||
*/
|
||||
data object TrashClick : VaultAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [VaultViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : VaultAction() {
|
||||
|
||||
/**
|
||||
* Indicates a vault data was received.
|
||||
*/
|
||||
data class VaultDataReceive(
|
||||
val vaultData: DataState<VaultData>,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
||||
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
||||
|
||||
/**
|
||||
* Transforms a [CipherView] into a [VaultState.ViewState.VaultItem].
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun CipherView.toVaultItem(): VaultState.ViewState.VaultItem =
|
||||
when (type) {
|
||||
CipherType.LOGIN -> VaultState.ViewState.VaultItem.Login(
|
||||
id = id.toString(),
|
||||
name = name.asText(),
|
||||
username = login?.username?.asText(),
|
||||
)
|
||||
|
||||
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(
|
||||
id = id.toString(),
|
||||
name = name.asText(),
|
||||
)
|
||||
|
||||
CipherType.CARD -> VaultState.ViewState.VaultItem.Card(
|
||||
id = id.toString(),
|
||||
name = name.asText(),
|
||||
brand = card?.brand?.asText(),
|
||||
lastFourDigits = card?.number
|
||||
?.takeLast(4)
|
||||
?.asText(),
|
||||
)
|
||||
|
||||
CipherType.IDENTITY -> VaultState.ViewState.VaultItem.Identity(
|
||||
id = id.toString(),
|
||||
name = name.asText(),
|
||||
firstName = identity?.firstName?.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms [VaultData] into [VaultState.ViewState].
|
||||
*/
|
||||
fun VaultData.toViewState(): VaultState.ViewState =
|
||||
if (cipherViewList.isEmpty() && folderViewList.isEmpty()) {
|
||||
VaultState.ViewState.NoItems
|
||||
} else {
|
||||
VaultState.ViewState.Content(
|
||||
loginItemsCount = cipherViewList.count { it.type == CipherType.LOGIN },
|
||||
cardItemsCount = cipherViewList.count { it.type == CipherType.CARD },
|
||||
identityItemsCount = cipherViewList.count { it.type == CipherType.IDENTITY },
|
||||
secureNoteItemsCount = cipherViewList.count { it.type == CipherType.SECURE_NOTE },
|
||||
favoriteItems = cipherViewList
|
||||
.filter { it.favorite }
|
||||
.map { it.toVaultItem() },
|
||||
folderItems = folderViewList.map { folderView ->
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = folderView.id,
|
||||
name = folderView.name.asText(),
|
||||
itemCount = cipherViewList.count { folderView.id == it.folderId },
|
||||
)
|
||||
},
|
||||
noFolderItems = cipherViewList
|
||||
.filter { it.folderId.isNullOrBlank() }
|
||||
.map { it.toVaultItem() },
|
||||
// TODO need to populate trash item count in BIT-969
|
||||
trashItemsCount = 0,
|
||||
)
|
||||
}
|
|
@ -45,18 +45,18 @@ class LocalDateTimeSerializerTest {
|
|||
LocalDateTimeData(
|
||||
dataAsLocalDateTime = LocalDateTime.of(
|
||||
2023,
|
||||
10,
|
||||
6,
|
||||
17,
|
||||
22,
|
||||
28,
|
||||
446666700,
|
||||
8,
|
||||
1,
|
||||
16,
|
||||
13,
|
||||
3,
|
||||
502391000,
|
||||
),
|
||||
),
|
||||
json.decodeFromString<LocalDateTimeData>(
|
||||
"""
|
||||
{
|
||||
"dataAsLocalDateTime": "2023-10-06T17:22:28.4466667Z"
|
||||
"dataAsLocalDateTime": "2023-08-01T16:13:03.502391Z"
|
||||
}
|
||||
""",
|
||||
),
|
||||
|
@ -69,7 +69,7 @@ class LocalDateTimeSerializerTest {
|
|||
json.parseToJsonElement(
|
||||
"""
|
||||
{
|
||||
"dataAsLocalDateTime": "2023-10-06T17:22:28.44Z"
|
||||
"dataAsLocalDateTime": "2023-10-06T17:22:28.4400000Z"
|
||||
}
|
||||
""",
|
||||
),
|
||||
|
|
|
@ -121,25 +121,49 @@ class VaultSdkSourceTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Cipher decryptListCollection should call SDK and return a Result with correct data`() =
|
||||
runBlocking {
|
||||
val mockCiphers = mockk<List<Cipher>>()
|
||||
val expectedResult = mockk<List<CipherListView>>()
|
||||
coEvery {
|
||||
clientVault.ciphers().decryptList(
|
||||
ciphers = mockCiphers,
|
||||
)
|
||||
} returns expectedResult
|
||||
val result = vaultSdkSource.decryptCipherListCollection(
|
||||
cipherList = mockCiphers,
|
||||
)
|
||||
assertEquals(
|
||||
expectedResult.asSuccess(),
|
||||
result,
|
||||
)
|
||||
coVerify {
|
||||
clientVault.ciphers().decryptList(
|
||||
ciphers = mockCiphers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Cipher decryptList should call SDK and return a Result with correct data`() = runBlocking {
|
||||
val mockCiphers = mockk<List<Cipher>>()
|
||||
val expectedResult = mockk<List<CipherListView>>()
|
||||
val mockCiphers = mockk<Cipher>()
|
||||
val expectedResult = mockk<CipherView>()
|
||||
coEvery {
|
||||
clientVault.ciphers().decryptList(
|
||||
ciphers = mockCiphers,
|
||||
clientVault.ciphers().decrypt(
|
||||
cipher = mockCiphers,
|
||||
)
|
||||
} returns expectedResult
|
||||
val result = vaultSdkSource.decryptCipherList(
|
||||
cipherList = mockCiphers,
|
||||
cipherList = listOf(mockCiphers),
|
||||
)
|
||||
assertEquals(
|
||||
expectedResult.asSuccess(),
|
||||
listOf(expectedResult).asSuccess(),
|
||||
result,
|
||||
)
|
||||
coVerify {
|
||||
clientVault.ciphers().decryptList(
|
||||
ciphers = mockCiphers,
|
||||
clientVault.ciphers().decrypt(
|
||||
cipher = mockCiphers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||
|
||||
import com.bitwarden.core.AttachmentView
|
||||
import com.bitwarden.core.CardView
|
||||
import com.bitwarden.core.CipherRepromptType
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.FieldType
|
||||
import com.bitwarden.core.FieldView
|
||||
import com.bitwarden.core.IdentityView
|
||||
import com.bitwarden.core.LoginUriView
|
||||
import com.bitwarden.core.LoginView
|
||||
import com.bitwarden.core.PasswordHistoryView
|
||||
import com.bitwarden.core.SecureNoteType
|
||||
import com.bitwarden.core.SecureNoteView
|
||||
import com.bitwarden.core.UriMatchType
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
/**
|
||||
* Create a mock [CipherView] with a given [number].
|
||||
*/
|
||||
fun createMockCipherView(number: Int): CipherView =
|
||||
CipherView(
|
||||
id = "mockId-$number",
|
||||
organizationId = "mockOrganizationId-$number",
|
||||
folderId = "mockId-$number",
|
||||
collectionIds = listOf("mockCollectionId-$number"),
|
||||
key = "mockKey-$number",
|
||||
name = "mockName-$number",
|
||||
notes = "mockNotes-$number",
|
||||
type = CipherType.LOGIN,
|
||||
login = createMockLoginView(number = number),
|
||||
creationDate = LocalDateTime
|
||||
.parse("2023-10-27T12:00:00")
|
||||
.toInstant(ZoneOffset.UTC),
|
||||
deletedDate = LocalDateTime
|
||||
.parse("2023-10-27T12:00:00")
|
||||
.toInstant(ZoneOffset.UTC),
|
||||
revisionDate = LocalDateTime
|
||||
.parse("2023-10-27T12:00:00")
|
||||
.toInstant(ZoneOffset.UTC),
|
||||
attachments = listOf(createMockAttachmentView(number = number)),
|
||||
card = createMockCardView(number = number),
|
||||
fields = listOf(createMockFieldView(number = number)),
|
||||
identity = createMockIdentityView(number = number),
|
||||
favorite = false,
|
||||
passwordHistory = listOf(createMockPasswordHistoryView(number = number)),
|
||||
reprompt = CipherRepromptType.NONE,
|
||||
secureNote = createMockSecureNoteView(),
|
||||
edit = false,
|
||||
organizationUseTotp = false,
|
||||
viewPassword = false,
|
||||
localData = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a mock [LoginView] with a given [number].
|
||||
*/
|
||||
fun createMockLoginView(number: Int): LoginView =
|
||||
LoginView(
|
||||
username = "mockUsername-$number",
|
||||
password = "mockPassword-$number",
|
||||
passwordRevisionDate = LocalDateTime
|
||||
.parse("2023-10-27T12:00:00")
|
||||
.toInstant(ZoneOffset.UTC),
|
||||
autofillOnPageLoad = false,
|
||||
uris = listOf(createMockUriView(number = number)),
|
||||
totp = "mockTotp-$number",
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a mock [LoginUriView] with a given [number].
|
||||
*/
|
||||
fun createMockUriView(number: Int): LoginUriView =
|
||||
LoginUriView(
|
||||
uri = "mockUri-$number",
|
||||
match = UriMatchType.HOST,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a mock [AttachmentView] with a given [number].
|
||||
*/
|
||||
fun createMockAttachmentView(number: Int): AttachmentView =
|
||||
AttachmentView(
|
||||
fileName = "mockFileName-$number",
|
||||
size = "1",
|
||||
sizeName = "mockSizeName-$number",
|
||||
id = "mockId-$number",
|
||||
url = "mockUrl-$number",
|
||||
key = "mockKey-$number",
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a mock [CardView] with a given [number].
|
||||
*/
|
||||
fun createMockCardView(number: Int): CardView =
|
||||
CardView(
|
||||
number = "mockNumber-$number",
|
||||
expMonth = "mockExpMonth-$number",
|
||||
code = "mockCode-$number",
|
||||
expYear = "mockExpirationYear-$number",
|
||||
cardholderName = "mockCardholderName-$number",
|
||||
brand = "mockBrand-$number",
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a mock [FieldView] with a given [number].
|
||||
*/
|
||||
fun createMockFieldView(number: Int): FieldView =
|
||||
FieldView(
|
||||
linkedId = 100U,
|
||||
name = "mockName-$number",
|
||||
type = FieldType.HIDDEN,
|
||||
value = "mockValue-$number",
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a mock [IdentityView] with a given [number].
|
||||
*/
|
||||
fun createMockIdentityView(number: Int): IdentityView =
|
||||
IdentityView(
|
||||
firstName = "mockFirstName-$number",
|
||||
middleName = "mockMiddleName-$number",
|
||||
lastName = "mockLastName-$number",
|
||||
passportNumber = "mockPassportNumber-$number",
|
||||
country = "mockCountry-$number",
|
||||
address1 = "mockAddress1-$number",
|
||||
address2 = "mockAddress2-$number",
|
||||
address3 = "mockAddress3-$number",
|
||||
city = "mockCity-$number",
|
||||
postalCode = "mockPostalCode-$number",
|
||||
title = "mockTitle-$number",
|
||||
ssn = "mockSsn-$number",
|
||||
phone = "mockPhone-$number",
|
||||
company = "mockCompany-$number",
|
||||
licenseNumber = "mockLicenseNumber-$number",
|
||||
state = "mockState-$number",
|
||||
email = "mockEmail-$number",
|
||||
username = "mockUsername-$number",
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a mock [PasswordHistoryView] with a given [number].
|
||||
*/
|
||||
fun createMockPasswordHistoryView(number: Int): PasswordHistoryView =
|
||||
PasswordHistoryView(
|
||||
password = "mockPassword-$number",
|
||||
lastUsedDate = LocalDateTime
|
||||
.parse("2023-10-27T12:00:00")
|
||||
.toInstant(ZoneOffset.UTC),
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a mock [SecureNoteView] with a given [number].
|
||||
*/
|
||||
fun createMockSecureNoteView(): SecureNoteView =
|
||||
SecureNoteView(
|
||||
type = SecureNoteType.GENERIC,
|
||||
)
|
|
@ -18,13 +18,18 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResul
|
|||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipher
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFolder
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -51,7 +56,7 @@ class VaultRepositoryTest {
|
|||
} returns Result.success(createMockSyncResponse(number = 1))
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
|
||||
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
|
||||
} returns listOf(createMockCipherView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
|
||||
} returns listOf(createMockFolderView(number = 1)).asSuccess()
|
||||
|
@ -70,7 +75,7 @@ class VaultRepositoryTest {
|
|||
assertEquals(
|
||||
DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherListViewList = listOf(createMockCipherListView(number = 1)),
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
),
|
||||
),
|
||||
|
@ -86,7 +91,7 @@ class VaultRepositoryTest {
|
|||
} returns Result.success(createMockSyncResponse(number = 1))
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
|
||||
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
|
||||
} returns listOf(createMockCipherView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
|
||||
} returns listOf(createMockFolderView(number = 1)).asSuccess()
|
||||
|
@ -101,7 +106,7 @@ class VaultRepositoryTest {
|
|||
assertEquals(
|
||||
DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherListViewList = listOf(createMockCipherListView(number = 1)),
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
),
|
||||
),
|
||||
|
@ -111,7 +116,7 @@ class VaultRepositoryTest {
|
|||
assertEquals(
|
||||
DataState.Pending(
|
||||
data = VaultData(
|
||||
cipherListViewList = listOf(createMockCipherListView(number = 1)),
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
),
|
||||
),
|
||||
|
@ -120,7 +125,7 @@ class VaultRepositoryTest {
|
|||
assertEquals(
|
||||
DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherListViewList = listOf(createMockCipherListView(number = 1)),
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
),
|
||||
),
|
||||
|
@ -161,7 +166,7 @@ class VaultRepositoryTest {
|
|||
} returns Result.success(createMockSyncResponse(number = 1))
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
|
||||
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
|
||||
} returns listOf(createMockCipherView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
|
||||
} returns mockException.asFailure()
|
||||
|
@ -221,7 +226,7 @@ class VaultRepositoryTest {
|
|||
} returns Result.success(createMockSyncResponse(number = 1))
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
|
||||
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
|
||||
} returns listOf(createMockCipherView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
|
||||
} returns listOf(createMockFolderView(number = 1)).asSuccess()
|
||||
|
@ -236,7 +241,7 @@ class VaultRepositoryTest {
|
|||
assertEquals(
|
||||
DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherListViewList = listOf(createMockCipherListView(number = 1)),
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
),
|
||||
),
|
||||
|
@ -249,7 +254,7 @@ class VaultRepositoryTest {
|
|||
assertEquals(
|
||||
DataState.Pending(
|
||||
data = VaultData(
|
||||
cipherListViewList = listOf(createMockCipherListView(number = 1)),
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
),
|
||||
),
|
||||
|
@ -258,7 +263,7 @@ class VaultRepositoryTest {
|
|||
assertEquals(
|
||||
DataState.NoNetwork(
|
||||
data = VaultData(
|
||||
cipherListViewList = listOf(createMockCipherListView(number = 1)),
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
),
|
||||
),
|
||||
|
@ -275,7 +280,7 @@ class VaultRepositoryTest {
|
|||
} returns Result.success(createMockSyncResponse(number = 1))
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
|
||||
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
|
||||
} returns listOf(createMockCipherView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
|
||||
} returns listOf(createMockFolderView(number = 1)).asSuccess()
|
||||
|
@ -310,6 +315,102 @@ class VaultRepositoryTest {
|
|||
coVerify { syncService.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync should be able to be called after unlockVaultAndSync is canceled`() = runTest {
|
||||
coEvery {
|
||||
syncService.sync()
|
||||
} returns Result.success(createMockSyncResponse(number = 1))
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
|
||||
} returns listOf(createMockCipherView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
|
||||
} returns listOf(createMockFolderView(number = 1)).asSuccess()
|
||||
fakeAuthDiskSource.storePrivateKey(
|
||||
userId = "mockUserId",
|
||||
privateKey = "mockPrivateKey-1",
|
||||
)
|
||||
fakeAuthDiskSource.storeUserKey(
|
||||
userId = "mockUserId",
|
||||
userKey = "mockKey-1",
|
||||
)
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
request = InitCryptoRequest(
|
||||
kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()),
|
||||
email = "email",
|
||||
password = "mockPassword-1",
|
||||
userKey = "mockKey-1",
|
||||
privateKey = "mockPrivateKey-1",
|
||||
organizationKeys = mapOf(),
|
||||
),
|
||||
)
|
||||
} coAnswers {
|
||||
delay(Long.MAX_VALUE)
|
||||
Result.success(InitializeCryptoResult.Success)
|
||||
}
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Unconfined)
|
||||
scope.launch {
|
||||
vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1")
|
||||
}
|
||||
coVerify(exactly = 0) { syncService.sync() }
|
||||
scope.cancel()
|
||||
vaultRepository.sync()
|
||||
|
||||
coVerify(exactly = 1) { syncService.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync should not be able to be called while unlockVaultAndSync is called`() = runTest {
|
||||
coEvery {
|
||||
syncService.sync()
|
||||
} returns Result.success(createMockSyncResponse(number = 1))
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
|
||||
} returns listOf(createMockCipherView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
|
||||
} returns listOf(createMockFolderView(number = 1)).asSuccess()
|
||||
fakeAuthDiskSource.storePrivateKey(
|
||||
userId = "mockUserId",
|
||||
privateKey = "mockPrivateKey-1",
|
||||
)
|
||||
fakeAuthDiskSource.storeUserKey(
|
||||
userId = "mockUserId",
|
||||
userKey = "mockKey-1",
|
||||
)
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
request = InitCryptoRequest(
|
||||
kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()),
|
||||
email = "email",
|
||||
password = "mockPassword-1",
|
||||
userKey = "mockKey-1",
|
||||
privateKey = "mockPrivateKey-1",
|
||||
organizationKeys = mapOf(),
|
||||
),
|
||||
)
|
||||
} coAnswers {
|
||||
delay(Long.MAX_VALUE)
|
||||
Result.success(InitializeCryptoResult.Success)
|
||||
}
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Unconfined)
|
||||
scope.launch {
|
||||
vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1")
|
||||
}
|
||||
// We call sync here but the call to the SyncService should be blocked
|
||||
// by the active call to unlockVaultAndSync
|
||||
vaultRepository.sync()
|
||||
|
||||
scope.cancel()
|
||||
|
||||
coVerify(exactly = 0) { syncService.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unlockVaultAndSync with initializeCrypto failure should return GenericError`() =
|
||||
runTest {
|
||||
|
@ -451,7 +552,7 @@ class VaultRepositoryTest {
|
|||
} returns Result.success(createMockSyncResponse(number = 1))
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1)))
|
||||
} returns listOf(createMockCipherListView(number = 1)).asSuccess()
|
||||
} returns listOf(createMockCipherView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1)))
|
||||
} returns listOf(createMockFolderView(number = 1)).asSuccess()
|
||||
|
@ -466,7 +567,7 @@ class VaultRepositoryTest {
|
|||
assertEquals(
|
||||
DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherListViewList = listOf(createMockCipherListView(number = 1)),
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -3,15 +3,31 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AccountSummary
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VaultViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableVaultDataStateFlow =
|
||||
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
|
||||
|
||||
private val vaultRepository: VaultRepository =
|
||||
mockk {
|
||||
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
|
||||
every { sync() } returns Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when not set`() {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -82,6 +98,113 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loaded with items should update state to Content`() = runTest {
|
||||
mutableVaultDataStateFlow.tryEmit(
|
||||
value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
assertEquals(
|
||||
createMockVaultState(
|
||||
viewState = VaultState.ViewState.Content(
|
||||
loginItemsCount = 1,
|
||||
cardItemsCount = 0,
|
||||
identityItemsCount = 0,
|
||||
secureNoteItemsCount = 0,
|
||||
favoriteItems = listOf(),
|
||||
folderItems = listOf(
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1".asText(),
|
||||
itemCount = 1,
|
||||
),
|
||||
),
|
||||
noFolderItems = listOf(),
|
||||
trashItemsCount = 0,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loaded with empty items should update state to NoItems`() = runTest {
|
||||
mutableVaultDataStateFlow.tryEmit(
|
||||
value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = emptyList(),
|
||||
folderViewList = emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
createMockVaultState(viewState = VaultState.ViewState.NoItems),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loading should update state to Loading`() = runTest {
|
||||
mutableVaultDataStateFlow.tryEmit(value = DataState.Loading)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
assertEquals(
|
||||
createMockVaultState(viewState = VaultState.ViewState.Loading),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Error should show toast and update state to NoItems`() = runTest {
|
||||
mutableVaultDataStateFlow.tryEmit(
|
||||
value = DataState.Error(
|
||||
error = IllegalStateException(),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultEvent.ShowToast("Vault error state not yet implemented"),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
createMockVaultState(viewState = VaultState.ViewState.NoItems),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow NoNetwork should show toast and update state to NoItems`() = runTest {
|
||||
mutableVaultDataStateFlow.tryEmit(
|
||||
value = DataState.NoNetwork(),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultEvent.ShowToast("Vault no network state not yet implemented"),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
createMockVaultState(viewState = VaultState.ViewState.NoItems),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AddItemClick should emit NavigateToAddItemScreen`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -175,12 +298,19 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
state: VaultState? = DEFAULT_STATE,
|
||||
): VaultViewModel = VaultViewModel(
|
||||
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE: VaultState = VaultState(
|
||||
avatarColorString = "FF0000FF",
|
||||
initials = "BW",
|
||||
accountSummaries = emptyList(),
|
||||
viewState = VaultState.ViewState.Loading,
|
||||
)
|
||||
private const val DEFAULT_COLOR_STRING: String = "FF0000FF"
|
||||
private const val DEFAULE_INITIALS: String = "BW"
|
||||
private val DEFAULT_STATE: VaultState =
|
||||
createMockVaultState(viewState = VaultState.ViewState.Loading)
|
||||
|
||||
private fun createMockVaultState(viewState: VaultState.ViewState): VaultState =
|
||||
VaultState(
|
||||
avatarColorString = DEFAULT_COLOR_STRING,
|
||||
initials = DEFAULE_INITIALS,
|
||||
accountSummaries = emptyList(),
|
||||
viewState = viewState,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VaultDataExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `toViewState should transform full VaultData into ViewState Content`() {
|
||||
val vaultData = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
)
|
||||
|
||||
val actual = vaultData.toViewState()
|
||||
|
||||
assertEquals(
|
||||
VaultState.ViewState.Content(
|
||||
loginItemsCount = 1,
|
||||
cardItemsCount = 0,
|
||||
identityItemsCount = 0,
|
||||
secureNoteItemsCount = 0,
|
||||
favoriteItems = listOf(),
|
||||
folderItems = listOf(
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1".asText(),
|
||||
itemCount = 1,
|
||||
),
|
||||
),
|
||||
noFolderItems = listOf(),
|
||||
trashItemsCount = 0,
|
||||
),
|
||||
actual,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should transform empty VaultData into ViewState NoItems`() {
|
||||
val vaultData = VaultData(
|
||||
cipherViewList = emptyList(),
|
||||
folderViewList = emptyList(),
|
||||
)
|
||||
|
||||
val actual = vaultData.toViewState()
|
||||
|
||||
assertEquals(
|
||||
VaultState.ViewState.NoItems,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue