mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 10:54:26 -05:00
BIT-1338 Finish Verification Code Screen Implementation (#721)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manages the flows for getting verification codes.
|
||||
*/
|
||||
interface TotpCodeManager {
|
||||
|
||||
/**
|
||||
* Flow for getting a DataState with multiple verification code items.
|
||||
*/
|
||||
fun getTotpCodesStateFlow(
|
||||
userId: String,
|
||||
cipherList: List<CipherView>,
|
||||
): StateFlow<DataState<List<VerificationCodeItem>>>
|
||||
|
||||
/**
|
||||
* Flow for getting a DataState with a single verification code item.
|
||||
*/
|
||||
fun getTotpCodeStateFlow(
|
||||
userId: String,
|
||||
cipher: CipherView,
|
||||
): StateFlow<DataState<VerificationCodeItem?>>
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.time.Clock
|
||||
|
||||
private const val ONE_SECOND_MILLISECOND = 1000L
|
||||
|
||||
/**
|
||||
* Primary implementation of [TotpCodeManager].
|
||||
*/
|
||||
class TotpCodeManagerImpl(
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
private val clock: Clock,
|
||||
) : TotpCodeManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
private val mutableVerificationCodeStateFlowMap =
|
||||
mutableMapOf<String, StateFlow<DataState<VerificationCodeItem?>>>()
|
||||
|
||||
override fun getTotpCodesStateFlow(
|
||||
userId: String,
|
||||
cipherList: List<CipherView>,
|
||||
): StateFlow<DataState<List<VerificationCodeItem>>> {
|
||||
// Generate state flows
|
||||
val stateFlows = cipherList.map { cipherView ->
|
||||
getTotpCodeStateFlowInternal(userId, cipherView)
|
||||
}
|
||||
return combine(stateFlows) { results ->
|
||||
when {
|
||||
results.any { it is DataState.Loading } -> {
|
||||
DataState.Loading
|
||||
}
|
||||
|
||||
else -> {
|
||||
DataState.Loaded(
|
||||
data = results.mapNotNull { (it as DataState.Loaded).data },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getTotpCodeStateFlow(
|
||||
userId: String,
|
||||
cipher: CipherView,
|
||||
): StateFlow<DataState<VerificationCodeItem?>> =
|
||||
getTotpCodeStateFlowInternal(
|
||||
userId = userId,
|
||||
cipher = cipher,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun getTotpCodeStateFlowInternal(
|
||||
userId: String,
|
||||
cipher: CipherView,
|
||||
): StateFlow<DataState<VerificationCodeItem?>> {
|
||||
val cipherId = cipher.id ?: return MutableStateFlow(DataState.Loaded(null))
|
||||
|
||||
return mutableVerificationCodeStateFlowMap.getOrPut(cipherId) {
|
||||
flow<DataState<VerificationCodeItem?>> {
|
||||
|
||||
val totpCode = cipher
|
||||
.login
|
||||
?.totp
|
||||
?: run {
|
||||
emit(DataState.Loaded(null))
|
||||
return@flow
|
||||
}
|
||||
|
||||
var item: VerificationCodeItem? = null
|
||||
while (currentCoroutineContext().isActive) {
|
||||
val time = (clock.millis() / ONE_SECOND_MILLISECOND).toInt()
|
||||
val dateTime = DateTime.now()
|
||||
if (item == null || item.isExpired(clock = clock)) {
|
||||
vaultSdkSource
|
||||
.generateTotp(
|
||||
totp = totpCode,
|
||||
userId = userId,
|
||||
time = dateTime,
|
||||
)
|
||||
.onSuccess { response ->
|
||||
item = VerificationCodeItem(
|
||||
code = response.code,
|
||||
totpCode = totpCode,
|
||||
periodSeconds = response.period.toInt(),
|
||||
timeLeftSeconds = response.period.toInt() -
|
||||
time % response.period.toInt(),
|
||||
issueTime = clock.millis(),
|
||||
uriLoginViewList = cipher.login?.uris,
|
||||
id = cipherId,
|
||||
name = cipher.name,
|
||||
username = cipher.login?.username,
|
||||
)
|
||||
}
|
||||
.onFailure {
|
||||
emit(DataState.Loaded(null))
|
||||
return@flow
|
||||
}
|
||||
} else {
|
||||
item?.let {
|
||||
item = it.copy(
|
||||
timeLeftSeconds = it.periodSeconds -
|
||||
(time % it.periodSeconds),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item?.let {
|
||||
emit(DataState.Loaded(it))
|
||||
}
|
||||
delay(ONE_SECOND_MILLISECOND)
|
||||
}
|
||||
}
|
||||
.onCompletion {
|
||||
mutableVerificationCodeStateFlowMap.remove(cipherId)
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun VerificationCodeItem.isExpired(clock: Clock): Boolean {
|
||||
val timeExpired = issueTime + (timeLeftSeconds * ONE_SECOND_MILLISECOND)
|
||||
return timeExpired < clock.millis()
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManagerImpl
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl
|
||||
import dagger.Module
|
||||
@@ -16,6 +18,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@@ -49,4 +52,17 @@ object VaultManagerModule {
|
||||
userLogoutManager = userLogoutManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTotpManager(
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
clock: Clock,
|
||||
): TotpCodeManager =
|
||||
TotpCodeManagerImpl(
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager.model
|
||||
|
||||
import com.bitwarden.core.LoginUriView
|
||||
|
||||
/**
|
||||
* Models the items returned by the TotpCodeManager.
|
||||
*
|
||||
* @property code The verification code for the item.
|
||||
* @property totpCode The totp code for the item.
|
||||
* @property periodSeconds The time span where the code is valid in seconds.
|
||||
* @property timeLeftSeconds The seconds remaining until a new code is required.
|
||||
* @property issueTime The time the verification code was issued.
|
||||
* @property uriLoginViewList The [LoginUriView] for the login item.
|
||||
* @property id The cipher id of the item.
|
||||
* @property name The name of the cipher item.
|
||||
* @property username The username associated with the item.
|
||||
*/
|
||||
data class VerificationCodeItem(
|
||||
val code: String,
|
||||
val totpCode: String,
|
||||
val periodSeconds: Int,
|
||||
val timeLeftSeconds: Int,
|
||||
val issueTime: Long,
|
||||
val uriLoginViewList: List<LoginUriView>?,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val username: String?,
|
||||
)
|
||||
@@ -10,6 +10,7 @@ import com.bitwarden.core.SendView
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
|
||||
@@ -116,6 +117,12 @@ interface VaultRepository : VaultLockManager {
|
||||
*/
|
||||
fun getSendStateFlow(sendId: String): StateFlow<DataState<SendView?>>
|
||||
|
||||
/**
|
||||
* Flow that represents the data for the TOTP verification codes for ciphers items.
|
||||
* This may emit an empty list if any issues arise during code generation.
|
||||
*/
|
||||
fun getAuthCodesFlow(): StateFlow<DataState<List<VerificationCodeItem>>>
|
||||
|
||||
/**
|
||||
* Emits the totp code result flow to listeners.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.vault.repository
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.CollectionView
|
||||
import com.bitwarden.core.DateTime
|
||||
@@ -33,7 +34,9 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
|
||||
@@ -56,7 +59,9 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -65,6 +70,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -93,6 +99,7 @@ class VaultRepositoryImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val fileManager: FileManager,
|
||||
private val vaultLockManager: VaultLockManager,
|
||||
private val totpCodeManager: TotpCodeManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : VaultRepository,
|
||||
VaultLockManager by vaultLockManager {
|
||||
@@ -293,6 +300,47 @@ class VaultRepositoryImpl(
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getAuthCodesFlow(): StateFlow<DataState<List<VerificationCodeItem>>> {
|
||||
val userId = requireNotNull(activeUserId)
|
||||
return vaultDataStateFlow
|
||||
.map { dataState ->
|
||||
dataState.map { vaultData ->
|
||||
vaultData
|
||||
.cipherViewList
|
||||
.filter {
|
||||
it.type == CipherType.LOGIN &&
|
||||
!it.login?.totp.isNullOrBlank() &&
|
||||
it.deletedDate == null
|
||||
}
|
||||
.toFilteredList(vaultFilterType)
|
||||
}
|
||||
}
|
||||
.flatMapLatest { cipherDataState ->
|
||||
val cipherList = cipherDataState.data ?: emptyList()
|
||||
totpCodeManager
|
||||
.getTotpCodesStateFlow(
|
||||
userId = userId,
|
||||
cipherList = cipherList,
|
||||
)
|
||||
.map { verificationCodeDataStates ->
|
||||
combineDataStates(
|
||||
verificationCodeDataStates,
|
||||
cipherDataState,
|
||||
) { verificationCodeItems, _ ->
|
||||
// Just return the verification items; we are only combining the
|
||||
// DataStates to know the overall state.
|
||||
verificationCodeItems
|
||||
}
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
override fun emitTotpCodeResult(totpCodeResult: TotpCodeResult) {
|
||||
mutableTotpCodeResultFlow.tryEmit(totpCodeResult)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepositoryImpl
|
||||
@@ -36,6 +37,7 @@ object VaultRepositoryModule {
|
||||
fileManager: FileManager,
|
||||
vaultLockManager: VaultLockManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
totpCodeManager: TotpCodeManager,
|
||||
): VaultRepository = VaultRepositoryImpl(
|
||||
syncService = syncService,
|
||||
sendsService = sendsService,
|
||||
@@ -46,5 +48,6 @@ object VaultRepositoryModule {
|
||||
fileManager = fileManager,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
totpCodeManager = totpCodeManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -134,8 +134,6 @@ fun VerificationCodeScreen(
|
||||
is VerificationCodeState.ViewState.Loading -> {
|
||||
BitwardenLoadingContent(modifier = modifier)
|
||||
}
|
||||
|
||||
is VerificationCodeState.ViewState.NoItems -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,21 @@ package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
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.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.toVerificationCodeViewState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -65,8 +63,12 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
vaultRepository
|
||||
.vaultDataStateFlow
|
||||
.onEach { sendAction(VerificationCodeAction.Internal.VaultDataReceive(vaultData = it)) }
|
||||
.getAuthCodesFlow()
|
||||
.onEach {
|
||||
sendAction(
|
||||
VerificationCodeAction.Internal.AuthCodesReceive(it),
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
@@ -135,16 +137,13 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
private fun handleInternalAction(action: VerificationCodeAction.Internal) {
|
||||
when (action) {
|
||||
is VerificationCodeAction.Internal.IconLoadingSettingReceive ->
|
||||
handleIconsSettingReceived(
|
||||
action,
|
||||
)
|
||||
handleIconsSettingReceived(action)
|
||||
|
||||
is VerificationCodeAction.Internal.PullToRefreshEnableReceive ->
|
||||
handlePullToRefreshEnableReceive(
|
||||
action,
|
||||
)
|
||||
handlePullToRefreshEnableReceive(action)
|
||||
|
||||
is VerificationCodeAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
is VerificationCodeAction.Internal.AuthCodesReceive ->
|
||||
handleAuthCodeReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,10 +153,6 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
mutableStateFlow.update {
|
||||
it.copy(isIconLoadingDisabled = action.isIconLoadingDisabled)
|
||||
}
|
||||
|
||||
vaultRepository.vaultDataStateFlow.value.data?.let { vaultData ->
|
||||
updateStateWithVaultData(vaultData, clearDialogState = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePullToRefreshEnableReceive(
|
||||
@@ -168,24 +163,45 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVaultDataReceive(action: VerificationCodeAction.Internal.VaultDataReceive) {
|
||||
updateViewState(action.vaultData)
|
||||
private fun handleAuthCodeReceive(action: VerificationCodeAction.Internal.AuthCodesReceive) {
|
||||
updateViewState(action.verificationCodeData)
|
||||
}
|
||||
//endregion VerificationCode Handlers
|
||||
|
||||
private fun updateViewState(vaultData: DataState<VaultData>) {
|
||||
when (vaultData) {
|
||||
is DataState.Error -> vaultErrorReceive(vaultData)
|
||||
is DataState.Loaded -> vaultLoadedReceive(vaultData = vaultData)
|
||||
is DataState.Loading -> vaultLoadingReceive()
|
||||
is DataState.NoNetwork -> vaultNoNetworkReceive(vaultData)
|
||||
is DataState.Pending -> vaultPendingReceive(vaultData)
|
||||
private fun updateViewState(
|
||||
verificationCodeData: DataState<List<VerificationCodeItem>>,
|
||||
) {
|
||||
when (verificationCodeData) {
|
||||
is DataState.Loaded -> {
|
||||
vaultLoadedReceive(verificationCodeData)
|
||||
}
|
||||
|
||||
is DataState.Error -> {
|
||||
vaultErrorReceive(verificationCodeData)
|
||||
}
|
||||
|
||||
is DataState.Loading -> {
|
||||
vaultLoadingReceive()
|
||||
}
|
||||
|
||||
is DataState.NoNetwork -> {
|
||||
vaultNoNetworkReceive(verificationCodeData)
|
||||
}
|
||||
|
||||
is DataState.Pending -> {
|
||||
vaultPendingReceive(verificationCodeData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun vaultNoNetworkReceive(vaultData: DataState.NoNetwork<VaultData>) {
|
||||
if (vaultData.data != null) {
|
||||
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
|
||||
private fun vaultNoNetworkReceive(
|
||||
verificationCodeData:
|
||||
DataState.NoNetwork<List<VerificationCodeItem>>,
|
||||
) {
|
||||
if (verificationCodeData.data != null) {
|
||||
updateStateWithVerificationCodeData(
|
||||
verificationCodeData = verificationCodeData.data,
|
||||
clearDialogState = true,
|
||||
)
|
||||
} else {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
@@ -201,12 +217,23 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
sendEvent(VerificationCodeEvent.DismissPullToRefresh)
|
||||
}
|
||||
|
||||
private fun vaultPendingReceive(vaultData: DataState.Pending<VaultData>) {
|
||||
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = false)
|
||||
private fun vaultPendingReceive(
|
||||
verificationCodeData: DataState.Pending<List<VerificationCodeItem>>,
|
||||
) {
|
||||
updateStateWithVerificationCodeData(
|
||||
verificationCodeData = verificationCodeData.data,
|
||||
clearDialogState = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun vaultLoadedReceive(vaultData: DataState.Loaded<VaultData>) {
|
||||
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
|
||||
private fun vaultLoadedReceive(
|
||||
verificationCodeData:
|
||||
DataState.Loaded<List<VerificationCodeItem>>,
|
||||
) {
|
||||
updateStateWithVerificationCodeData(
|
||||
verificationCodeData = verificationCodeData.data,
|
||||
clearDialogState = true,
|
||||
)
|
||||
sendEvent(VerificationCodeEvent.DismissPullToRefresh)
|
||||
}
|
||||
|
||||
@@ -214,9 +241,12 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
mutableStateFlow.update { it.copy(viewState = VerificationCodeState.ViewState.Loading) }
|
||||
}
|
||||
|
||||
private fun vaultErrorReceive(vaultData: DataState.Error<VaultData>) {
|
||||
private fun vaultErrorReceive(vaultData: DataState.Error<List<VerificationCodeItem>>) {
|
||||
if (vaultData.data != null) {
|
||||
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = true)
|
||||
updateStateWithVerificationCodeData(
|
||||
verificationCodeData = vaultData.data,
|
||||
clearDialogState = true,
|
||||
)
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
@@ -230,35 +260,40 @@ class VerificationCodeViewModel @Inject constructor(
|
||||
sendEvent(VerificationCodeEvent.DismissPullToRefresh)
|
||||
}
|
||||
|
||||
private fun updateStateWithVaultData(
|
||||
vaultData: VaultData,
|
||||
private fun updateStateWithVerificationCodeData(
|
||||
verificationCodeData: List<VerificationCodeItem>,
|
||||
clearDialogState: Boolean,
|
||||
) {
|
||||
val viewState = vaultData
|
||||
.cipherViewList
|
||||
.filter {
|
||||
it.type == CipherType.LOGIN &&
|
||||
!it.login?.totp.isNullOrBlank() &&
|
||||
it.deletedDate == null
|
||||
}
|
||||
.toFilteredList(state.vaultFilterType)
|
||||
.toVerificationCodeViewState(
|
||||
baseIconUrl = state.baseIconUrl,
|
||||
isIconLoadingDisabled = state.isIconLoadingDisabled,
|
||||
)
|
||||
|
||||
if (viewState is VerificationCodeState.ViewState.NoItems) {
|
||||
if (verificationCodeData.isEmpty()) {
|
||||
sendEvent(VerificationCodeEvent.NavigateBack)
|
||||
return
|
||||
}
|
||||
|
||||
mutableStateFlow.update { state ->
|
||||
state.copy(
|
||||
viewState = viewState,
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
verificationCodeDisplayItems = verificationCodeData
|
||||
.map { item ->
|
||||
VerificationCodeDisplayItem(
|
||||
id = item.id,
|
||||
authCode = item.code,
|
||||
label = item.name,
|
||||
supportingLabel = item.username,
|
||||
periodSeconds = item.periodSeconds,
|
||||
timeLeftSeconds = item.timeLeftSeconds,
|
||||
startIcon = item.uriLoginViewList.toLoginIconData(
|
||||
baseIconUrl = state.baseIconUrl,
|
||||
isIconLoadingDisabled = state.isIconLoadingDisabled,
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
dialogState = state.dialogState.takeUnless { clearDialogState },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//endregion VerificationCode Handlers
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,9 +341,13 @@ data class VerificationCodeState(
|
||||
abstract val isPullToRefreshEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Represents a state where the [VerificationCodeScreen] has no items to display.
|
||||
* Represents an error state for the [VerificationCodeScreen].
|
||||
*
|
||||
* @property message Error message to display.
|
||||
*/
|
||||
data object NoItems : ViewState() {
|
||||
data class Error(
|
||||
val message: Text,
|
||||
) : ViewState() {
|
||||
override val isPullToRefreshEnabled: Boolean get() = true
|
||||
}
|
||||
|
||||
@@ -320,17 +359,6 @@ data class VerificationCodeState(
|
||||
override val isPullToRefreshEnabled: Boolean get() = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an error state for the [VerificationCodeScreen].
|
||||
*
|
||||
* @property message Error message to display.
|
||||
*/
|
||||
data class Error(
|
||||
val message: Text,
|
||||
) : ViewState() {
|
||||
override val isPullToRefreshEnabled: Boolean get() = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Content state for the [VerificationCodeScreen] showing the actual content or items.
|
||||
*/
|
||||
@@ -451,10 +479,10 @@ sealed class VerificationCodeAction {
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a vault data was received.
|
||||
* Indicates the verification code data was received.
|
||||
*/
|
||||
data class VaultDataReceive(
|
||||
val vaultData: DataState<VaultData>,
|
||||
data class AuthCodesReceive(
|
||||
val verificationCodeData: DataState<List<VerificationCodeItem>>,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.verificationcode.util
|
||||
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.VerificationCodeDisplayItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.VerificationCodeState
|
||||
|
||||
/**
|
||||
* Converts a list of [CipherView] to a list of [VerificationCodeDisplayItem].
|
||||
*/
|
||||
fun List<CipherView>.toVerificationCodeViewState(
|
||||
baseIconUrl: String,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
): VerificationCodeState.ViewState =
|
||||
if (isNotEmpty()) {
|
||||
VerificationCodeState.ViewState.Content(
|
||||
verificationCodeDisplayItems = toDisplayItemList(
|
||||
baseIconUrl = baseIconUrl,
|
||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
VerificationCodeState.ViewState.NoItems
|
||||
}
|
||||
|
||||
private fun List<CipherView>.toDisplayItemList(
|
||||
baseIconUrl: String,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
): List<VerificationCodeDisplayItem> =
|
||||
this.map {
|
||||
it.toDisplayItem(
|
||||
baseIconUrl = baseIconUrl,
|
||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A function used to create a sample [VerificationCodeDisplayItem].
|
||||
*/
|
||||
fun CipherView.toDisplayItem(
|
||||
baseIconUrl: String,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
): VerificationCodeDisplayItem =
|
||||
VerificationCodeDisplayItem(
|
||||
id = id.orEmpty(),
|
||||
authCode = "123456",
|
||||
label = name,
|
||||
supportingLabel = login?.username,
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
startIcon = login?.uris.toLoginIconData(
|
||||
baseIconUrl = baseIconUrl,
|
||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.x8bit.bitwarden.data.vault.manager
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.TotpResponse
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class TotpCodeManagerTest {
|
||||
private val userId = "userId"
|
||||
private val cipherList = listOf(
|
||||
createMockCipherView(1, isDeleted = false),
|
||||
)
|
||||
|
||||
private val vaultSdkSource: VaultSdkSource = mockk()
|
||||
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
|
||||
private val clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
private val totpCodeManager: TotpCodeManager = TotpCodeManagerImpl(
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
clock = clock,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `getTotpCodeStateFlow should have loaded data with a valid values passed in`() = runTest {
|
||||
val totpResponse = TotpResponse("123456", 30u)
|
||||
coEvery { vaultSdkSource.generateTotp(any(), any(), any()) } returns Result.success(
|
||||
totpResponse,
|
||||
)
|
||||
|
||||
val expected = createVerificationCodeItem()
|
||||
|
||||
totpCodeManager.getTotpCodesStateFlow(userId, cipherList).test {
|
||||
assertEquals(DataState.Loaded(listOf(expected)), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getTotpCodeStateFlow should have loaded data with empty list if no totp code is provided`() =
|
||||
runTest {
|
||||
val totpResponse = TotpResponse("123456", 30u)
|
||||
coEvery { vaultSdkSource.generateTotp(any(), any(), any()) } returns Result.success(
|
||||
totpResponse,
|
||||
)
|
||||
|
||||
val cipherView = createMockCipherView(1).copy(
|
||||
login = createMockLoginView(1).copy(
|
||||
totp = null,
|
||||
),
|
||||
)
|
||||
|
||||
totpCodeManager.getTotpCodesStateFlow(userId, listOf(cipherView)).test {
|
||||
assertEquals(DataState.Loaded(emptyList<VerificationCodeItem>()), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getTotpCodesStateFlow should have loaded data with empty list if unable to generate auth code`() =
|
||||
runTest {
|
||||
coEvery { vaultSdkSource.generateTotp(any(), any(), any()) } returns
|
||||
Result.failure(
|
||||
exception = Exception(),
|
||||
)
|
||||
|
||||
val cipherView = createMockCipherView(1).copy(
|
||||
login = createMockLoginView(1).copy(
|
||||
totp = null,
|
||||
),
|
||||
)
|
||||
|
||||
totpCodeManager.getTotpCodesStateFlow(userId, listOf(cipherView)).test {
|
||||
assertEquals(DataState.Loaded(emptyList<VerificationCodeItem>()), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTotpCodeStateFlow should have loaded item with a valid data passed in`() = runTest {
|
||||
|
||||
val totpResponse = TotpResponse("123456", 30u)
|
||||
coEvery { vaultSdkSource.generateTotp(any(), any(), any()) } returns Result.success(
|
||||
totpResponse,
|
||||
)
|
||||
|
||||
val cipherView = createMockCipherView(1)
|
||||
|
||||
val expected = createVerificationCodeItem()
|
||||
|
||||
totpCodeManager.getTotpCodeStateFlow(userId, cipherView).test {
|
||||
assertEquals(DataState.Loaded(expected), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTotpCodeFlow should have null data if unable to get item`() =
|
||||
runTest {
|
||||
val totpResponse = TotpResponse("123456", 30u)
|
||||
coEvery { vaultSdkSource.generateTotp(any(), any(), any()) } returns Result.success(
|
||||
totpResponse,
|
||||
)
|
||||
|
||||
val cipherView = createMockCipherView(1).copy(
|
||||
login = null,
|
||||
)
|
||||
|
||||
totpCodeManager.getTotpCodeStateFlow(userId, cipherView).test {
|
||||
assertEquals(DataState.Loaded(null), awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,9 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFolder
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkSend
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
|
||||
@@ -74,6 +76,7 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem
|
||||
import io.mockk.awaits
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
@@ -104,6 +107,7 @@ class VaultRepositoryTest {
|
||||
private val sendsService: SendsService = mockk()
|
||||
private val ciphersService: CiphersService = mockk()
|
||||
private val vaultDiskSource: VaultDiskSource = mockk()
|
||||
private val totpCodeManager: TotpCodeManager = mockk()
|
||||
private val vaultSdkSource: VaultSdkSource = mockk {
|
||||
every { clearCrypto(userId = any()) } just runs
|
||||
}
|
||||
@@ -130,6 +134,7 @@ class VaultRepositoryTest {
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultLockManager = vaultLockManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
totpCodeManager = totpCodeManager,
|
||||
fileManager = fileManager,
|
||||
)
|
||||
|
||||
@@ -2196,6 +2201,60 @@ class VaultRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getVerificationCodesFlow should update data state when state changes`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val userId = "mockId-1"
|
||||
|
||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||
coEvery { syncService.sync() } returns mockSyncResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(
|
||||
organizationKeys = createMockOrganizationKeys(1),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultDiskSource.replaceVaultData(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
vault = mockSyncResponse,
|
||||
)
|
||||
} just runs
|
||||
|
||||
val stateFlow = MutableStateFlow<DataState<List<VerificationCodeItem>>>(
|
||||
DataState.Loading,
|
||||
)
|
||||
|
||||
every {
|
||||
totpCodeManager.getTotpCodesStateFlow(userId = userId, any())
|
||||
} returns stateFlow
|
||||
|
||||
setupDataStateFlow(userId = userId)
|
||||
|
||||
vaultRepository.getAuthCodesFlow().test {
|
||||
assertEquals(
|
||||
DataState.Loading,
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
stateFlow.tryEmit(DataState.Loaded(listOf(createVerificationCodeItem())))
|
||||
|
||||
assertEquals(
|
||||
DataState.Loaded(listOf(createVerificationCodeItem())),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
assertEquals(
|
||||
DataState.Pending(listOf(createVerificationCodeItem())),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
||||
/**
|
||||
@@ -2348,6 +2407,63 @@ class VaultRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setupDataStateFlow(userId: String) {
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipherList(
|
||||
userId = userId,
|
||||
cipherList = listOf(createMockSdkCipher(1)),
|
||||
)
|
||||
} returns listOf(createMockCipherView(1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptFolderList(
|
||||
userId = userId,
|
||||
folderList = listOf(createMockSdkFolder(1)),
|
||||
)
|
||||
} returns listOf(createMockFolderView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCollectionList(
|
||||
userId = userId,
|
||||
collectionList = listOf(createMockSdkCollection(1)),
|
||||
)
|
||||
} returns listOf(createMockCollectionView(number = 1)).asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptSendList(
|
||||
userId = userId,
|
||||
sendList = listOf(createMockSdkSend(number = 1)),
|
||||
)
|
||||
} returns listOf(createMockSendView(number = 1)).asSuccess()
|
||||
val ciphersFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Cipher>>()
|
||||
val collectionsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Collection>>()
|
||||
val foldersFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Folder>>()
|
||||
val sendsFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Send>>()
|
||||
setupVaultDiskSourceFlows(
|
||||
ciphersFlow = ciphersFlow,
|
||||
collectionsFlow = collectionsFlow,
|
||||
foldersFlow = foldersFlow,
|
||||
sendsFlow = sendsFlow,
|
||||
)
|
||||
|
||||
vaultRepository.vaultDataStateFlow.test {
|
||||
ciphersFlow.tryEmit(listOf(createMockCipher(1)))
|
||||
collectionsFlow.tryEmit(listOf(createMockCollection(number = 1)))
|
||||
foldersFlow.tryEmit(listOf(createMockFolder(number = 1)))
|
||||
sendsFlow.tryEmit(listOf(createMockSend(number = 1)))
|
||||
assertEquals(DataState.Loading, awaitItem())
|
||||
|
||||
assertEquals(
|
||||
DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMockUri(
|
||||
url: String,
|
||||
queryParams: Map<String, String> = emptyMap(),
|
||||
@@ -2367,7 +2483,7 @@ class VaultRepositoryTest {
|
||||
return mockInstant
|
||||
}
|
||||
|
||||
//endregion Helper functions
|
||||
//endregion Helper functions
|
||||
}
|
||||
|
||||
private val MOCK_PROFILE = AccountJson.Profile(
|
||||
|
||||
@@ -107,7 +107,7 @@ class VerificationCodeScreenTest : BaseComposeTest() {
|
||||
.onNodeWithText(message)
|
||||
.assertIsNotDisplayed()
|
||||
|
||||
mutableStateFlow.update { it.copy(viewState = VerificationCodeState.ViewState.NoItems) }
|
||||
mutableStateFlow.update { it.copy(viewState = VerificationCodeState.ViewState.Loading) }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(message)
|
||||
|
||||
@@ -10,16 +10,14 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
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 com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.toDisplayItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
@@ -28,6 +26,7 @@ import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -39,11 +38,12 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val clipboardManager: BitwardenClipboardManager = mockk()
|
||||
|
||||
private val mutableVaultDataStateFlow =
|
||||
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
|
||||
private val mutableAuthCodeFlow =
|
||||
MutableStateFlow<DataState<List<VerificationCodeItem>>>(DataState.Loading)
|
||||
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { vaultFilterType } returns VaultFilterType.AllVaults
|
||||
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
|
||||
every { getAuthCodesFlow() } returns mutableAuthCodeFlow.asStateFlow()
|
||||
every { sync() } just runs
|
||||
}
|
||||
|
||||
@@ -99,22 +99,11 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on ItemClick should emit ItemClick`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val testId = "testId"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VerificationCodeAction.ItemClick(testId))
|
||||
assertEquals(VerificationCodeEvent.NavigateToVaultItem(testId), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SearchIconClick should emit NavigateToVaultSearchScreen`() = runTest {
|
||||
fun `ItemClick for vault item should emit NavigateToVaultItem`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VerificationCodeAction.SearchIconClick)
|
||||
assertEquals(VerificationCodeEvent.NavigateToVaultSearchScreen, awaitItem())
|
||||
viewModel.actionChannel.trySend(VerificationCodeAction.ItemClick(id = "mock"))
|
||||
assertEquals(VerificationCodeEvent.NavigateToVaultItem(id = "mock"), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +119,22 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RefreshClick should sync`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.actionChannel.trySend(VerificationCodeAction.RefreshClick)
|
||||
verify { vaultRepository.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SearchIconClick should emit NavigateToVaultSearchScreen`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VerificationCodeAction.SearchIconClick)
|
||||
assertEquals(VerificationCodeEvent.NavigateToVaultSearchScreen, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SyncClick should display the loading dialog and call sync`() {
|
||||
val viewModel = createViewModel()
|
||||
@@ -149,52 +154,23 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ItemClick for vault item should emit NavigateToVaultItem`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VerificationCodeAction.ItemClick(id = "mock"))
|
||||
assertEquals(VerificationCodeEvent.NavigateToVaultItem(id = "mock"), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RefreshClick should sync`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.actionChannel.trySend(VerificationCodeAction.RefreshClick)
|
||||
verify { vaultRepository.sync() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Pending with data should update state to Content`() = runTest {
|
||||
fun `AuthCodeFlow Pending with data should update state to Content`() {
|
||||
setupMockUri()
|
||||
|
||||
mutableVaultDataStateFlow.tryEmit(
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
value = DataState.Pending(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
data = listOf(createVerificationCodeItem()),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
assertEquals(
|
||||
createVerificationCodeState(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
verificationCodeDisplayItems = listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
isDeleted = false,
|
||||
)
|
||||
.toDisplayItem(
|
||||
baseIconUrl = initialState.baseIconUrl,
|
||||
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
|
||||
),
|
||||
),
|
||||
createDisplayItemList(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
@@ -203,56 +179,90 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultDataStateFlow Pending with empty data should call NavigateBack to go to the vault screen`() =
|
||||
fun `AuthCodeFlow Pending with no data should call NavigateBack to go to the vault screen`() =
|
||||
runTest {
|
||||
val dataState = DataState.Pending(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = true)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
setupMockUri()
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
value = DataState.Pending(
|
||||
data = listOf(),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultDataStateFlow Pending with trash data should call NavigateBack event`() = runTest {
|
||||
val dataState = DataState.Pending(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = true)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
fun `AuthCodeFlow Error with data should update state to Content`() = runTest {
|
||||
setupMockUri()
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
value = DataState.Error(
|
||||
data = listOf(createVerificationCodeItem()),
|
||||
error = IllegalStateException(),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Error without data should update state to Error`() = runTest {
|
||||
val dataState = DataState.Error<VaultData>(
|
||||
error = IllegalStateException(),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
createVerificationCodeState(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
createDisplayItemList(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `AuthCodeFlow Error with no data should call NavigateBack to go to the vault screen`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
value = DataState.Error(
|
||||
data = listOf(),
|
||||
error = IllegalStateException(),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `AuthCodeFlow Error with null data should show error screen`() = runTest {
|
||||
setupMockUri()
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
value = DataState.Error(
|
||||
data = null,
|
||||
error = IllegalStateException(),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
createVerificationCodeState(
|
||||
viewState = VerificationCodeState.ViewState.Error(
|
||||
@@ -263,101 +273,30 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Error with data should update state to Content`() = runTest {
|
||||
setupMockUri()
|
||||
|
||||
val dataState = DataState.Error(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
error = IllegalStateException(),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
assertEquals(
|
||||
createVerificationCodeState(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
verificationCodeDisplayItems = listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
isDeleted = false,
|
||||
)
|
||||
.toDisplayItem(
|
||||
baseIconUrl = initialState.baseIconUrl,
|
||||
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultDataStateFlow Error with empty data should call NavigateBack to go the vault screen`() =
|
||||
fun `AuthCodeFlow NoNetwork with empty data should call NavigateBack to go to the vault screen`() =
|
||||
runTest {
|
||||
val dataState = DataState.Error(
|
||||
data = VaultData(
|
||||
cipherViewList = emptyList(),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
error = IllegalStateException(),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultDataStateFlow Error with trash data should call NavigateBack to go to the vault screen`() =
|
||||
runTest {
|
||||
val dataState = DataState.Error(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = true)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
error = IllegalStateException(),
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
DataState.NoNetwork(emptyList()),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow NoNetwork without data should update state to Error`() = runTest {
|
||||
val dataState = DataState.NoNetwork<VaultData>()
|
||||
|
||||
fun `AuthCodeFlow NoNetwork with null should update state to Error`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
DataState.NoNetwork(null),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
createVerificationCodeState(
|
||||
viewState = VerificationCodeState.ViewState.Error(
|
||||
@@ -371,37 +310,25 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow NoNetwork with data should update state to Content`() = runTest {
|
||||
fun `AuthCodeFlow NoNetwork with data should update state to Content`() = runTest {
|
||||
setupMockUri()
|
||||
|
||||
val dataState = DataState.NoNetwork(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = false)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
value = DataState.NoNetwork(
|
||||
listOf(createVerificationCodeItem()),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
createVerificationCodeState(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
verificationCodeDisplayItems = listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
isDeleted = false,
|
||||
)
|
||||
.toDisplayItem(
|
||||
baseIconUrl = initialState.baseIconUrl,
|
||||
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
|
||||
),
|
||||
),
|
||||
createDisplayItemList(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
@@ -409,113 +336,49 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow NoNetwork with trash data should call NavigateBack`() = runTest {
|
||||
val dataState = DataState.NoNetwork(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = true)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
fun `AuthCodeFlow Loaded with empty data should call NavigateBack to go the vault screen`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
DataState.Loaded(emptyList()),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuthCodeFlow Loaded with valid items should update ViewState to content`() = runTest {
|
||||
setupMockUri()
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
mutableAuthCodeFlow.tryEmit(
|
||||
value = DataState.Loaded(
|
||||
listOf(createVerificationCodeItem()),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
createVerificationCodeState(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
createDisplayItemList(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loaded with empty items should update call NavigateBack to go the vault screen`() =
|
||||
runTest {
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = emptyList(),
|
||||
folderViewList = emptyList(),
|
||||
collectionViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loaded with trash items should call NavigateBack to go to the vault screen`() =
|
||||
runTest {
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1, isDeleted = true)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
isDeleted = false,
|
||||
),
|
||||
),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
mutableVaultDataStateFlow.tryEmit(value = dataState)
|
||||
assertEquals(VerificationCodeEvent.DismissPullToRefresh, awaitItem())
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
createVerificationCodeState(
|
||||
viewState = VerificationCodeState.ViewState.Content(
|
||||
listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
isDeleted = false,
|
||||
)
|
||||
.toDisplayItem(
|
||||
baseIconUrl = initialState.baseIconUrl,
|
||||
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loading should update state to Loading`() = runTest {
|
||||
mutableVaultDataStateFlow.tryEmit(value = DataState.Loading)
|
||||
fun `AuthCodeFlow Loading should update state to Loading`() = runTest {
|
||||
mutableAuthCodeFlow.tryEmit(value = DataState.Loading)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
@@ -580,13 +443,33 @@ class VerificationCodeViewModelTest : BaseViewModelTest() {
|
||||
@Suppress("MaxLineLength")
|
||||
private fun createVerificationCodeState(
|
||||
viewState: VerificationCodeState.ViewState = VerificationCodeState.ViewState.Loading,
|
||||
): VerificationCodeState =
|
||||
VerificationCodeState(
|
||||
viewState = viewState,
|
||||
vaultFilterType = vaultRepository.vaultFilterType,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||
dialogState = null,
|
||||
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
|
||||
) = VerificationCodeState(
|
||||
viewState = viewState,
|
||||
vaultFilterType = vaultRepository.vaultFilterType,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||
dialogState = null,
|
||||
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
|
||||
)
|
||||
|
||||
private fun createDisplayItemList() = listOf(
|
||||
createMockCipherView(
|
||||
number = 1,
|
||||
isDeleted = false,
|
||||
)
|
||||
.let { cipherView ->
|
||||
VerificationCodeDisplayItem(
|
||||
id = cipherView.id.toString(),
|
||||
authCode = "123456",
|
||||
label = cipherView.name,
|
||||
supportingLabel = cipherView.login?.username,
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 30,
|
||||
startIcon = cipherView.login?.uris.toLoginIconData(
|
||||
isIconLoadingDisabled = initialState.isIconLoadingDisabled,
|
||||
baseIconUrl = initialState.baseIconUrl,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.verificationcode.util
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
|
||||
|
||||
fun createVerificationCodeItem() =
|
||||
VerificationCodeItem(
|
||||
code = "123456",
|
||||
totpCode = "mockTotp-1",
|
||||
periodSeconds = 30,
|
||||
id = "mockId-1",
|
||||
issueTime = 1698408000000,
|
||||
timeLeftSeconds = 30,
|
||||
name = "mockName-1",
|
||||
uriLoginViewList = createMockLoginView(1).uris,
|
||||
username = "mockUsername-1",
|
||||
)
|
||||
Reference in New Issue
Block a user