mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
BIT-1339 Add TOTP item to detail screen (#744)
This commit is contained in:
parent
6223b225c5
commit
dc3081c5d6
16 changed files with 756 additions and 123 deletions
|
@ -74,13 +74,12 @@ class TotpCodeManagerImpl(
|
|||
@Suppress("LongMethod")
|
||||
private fun getTotpCodeStateFlowInternal(
|
||||
userId: String,
|
||||
cipher: CipherView,
|
||||
cipher: CipherView?,
|
||||
): StateFlow<DataState<VerificationCodeItem?>> {
|
||||
val cipherId = cipher.id ?: return MutableStateFlow(DataState.Loaded(null))
|
||||
val cipherId = cipher?.id ?: return MutableStateFlow(DataState.Loaded(null))
|
||||
|
||||
return mutableVerificationCodeStateFlowMap.getOrPut(cipher) {
|
||||
flow<DataState<VerificationCodeItem?>> {
|
||||
|
||||
val totpCode = cipher
|
||||
.login
|
||||
?.totp
|
||||
|
|
|
@ -128,6 +128,12 @@ interface VaultRepository : VaultLockManager {
|
|||
*/
|
||||
fun getSendStateFlow(sendId: String): StateFlow<DataState<SendView?>>
|
||||
|
||||
/**
|
||||
* Flow that represents the data for a single verification code item.
|
||||
* This may emit null if any issues arise during code generation.
|
||||
*/
|
||||
fun getAuthCodeFlow(cipherId: String): StateFlow<DataState<VerificationCodeItem?>>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
@ -74,6 +74,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -363,6 +364,37 @@ class VaultRepositoryImpl(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getAuthCodeFlow(cipherId: String): StateFlow<DataState<VerificationCodeItem?>> {
|
||||
val userId = requireNotNull(activeUserId)
|
||||
return getVaultItemStateFlow(cipherId)
|
||||
.flatMapLatest { cipherDataState ->
|
||||
val cipher = cipherDataState.data
|
||||
?: return@flatMapLatest flowOf(DataState.Loaded(null))
|
||||
totpCodeManager
|
||||
.getTotpCodeStateFlow(
|
||||
userId = userId,
|
||||
cipher = cipher,
|
||||
)
|
||||
.map { totpCodeDataState ->
|
||||
val totpCodeData = totpCodeDataState.data
|
||||
combineDataStates(
|
||||
totpCodeDataState.map { Unit },
|
||||
cipherDataState,
|
||||
) { _, _ ->
|
||||
// Just return the verification items; we are only combining the
|
||||
// DataStates to know the overall state.
|
||||
totpCodeData
|
||||
}
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
override fun emitTotpCodeResult(totpCodeResult: TotpCodeResult) {
|
||||
mutableTotpCodeResultFlow.tryEmit(totpCodeResult)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components
|
||||
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* A countdown timer displayed to the user.
|
||||
*
|
||||
* @param timeLeftSeconds The seconds left on the timer.
|
||||
* @param periodSeconds The period for the timer countdown.
|
||||
* @param modifier A [Modifier] for the composable.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenCircularCountdownIndicator(
|
||||
timeLeftSeconds: Int,
|
||||
periodSeconds: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val progressAnimate by animateFloatAsState(
|
||||
targetValue = timeLeftSeconds.toFloat() / periodSeconds,
|
||||
animationSpec = tween(
|
||||
durationMillis = periodSeconds,
|
||||
delayMillis = 0,
|
||||
easing = LinearOutSlowInEasing,
|
||||
),
|
||||
)
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
progress = { progressAnimate },
|
||||
modifier = Modifier.size(size = 30.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = timeLeftSeconds.toString(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenCircularCountdownIndicator
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenHiddenPasswordField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenIconButtonWithResource
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
|
@ -28,6 +29,9 @@ import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
|||
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
|
||||
private const val AUTH_CODE_SPACING_INTERVAL = 3
|
||||
|
||||
/**
|
||||
* The top level content UI state for the [VaultItemScreen] when viewing a Login cipher.
|
||||
|
@ -94,14 +98,18 @@ fun VaultItemLoginContent(
|
|||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TotpField(
|
||||
isPremiumUser = loginItemState.isPremiumUser,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
loginItemState.totpCodeItemData?.let { totpCodeItemData ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TotpField(
|
||||
totpCodeItemData = totpCodeItemData,
|
||||
isPremiumUser = loginItemState.isPremiumUser,
|
||||
onCopyTotpClick = vaultLoginItemTypeHandlers.onCopyTotpCodeClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris ->
|
||||
|
@ -298,11 +306,37 @@ private fun PasswordHistoryCount(
|
|||
|
||||
@Composable
|
||||
private fun TotpField(
|
||||
totpCodeItemData: TotpCodeItemData,
|
||||
isPremiumUser: Boolean,
|
||||
onCopyTotpClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (isPremiumUser) {
|
||||
// TODO: Insert TOTP values here (BIT-1214)
|
||||
Row {
|
||||
BitwardenTextFieldWithActions(
|
||||
label = stringResource(id = R.string.verification_code_totp),
|
||||
value = totpCodeItemData.verificationCode
|
||||
.chunked(AUTH_CODE_SPACING_INTERVAL)
|
||||
.joinToString(" "),
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
singleLine = true,
|
||||
actions = {
|
||||
BitwardenCircularCountdownIndicator(
|
||||
timeLeftSeconds = totpCodeItemData.timeLeftSeconds,
|
||||
periodSeconds = totpCodeItemData.periodSeconds,
|
||||
)
|
||||
BitwardenIconButtonWithResource(
|
||||
iconRes = IconResource(
|
||||
iconPainter = painterResource(id = R.drawable.ic_copy),
|
||||
contentDescription = stringResource(id = R.string.copy_totp),
|
||||
),
|
||||
onClick = onCopyTotpClick,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.verification_code_totp),
|
||||
|
|
|
@ -10,6 +10,8 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.map
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult
|
||||
|
@ -18,6 +20,8 @@ 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.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.VaultItemStateData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
|
@ -58,10 +62,28 @@ class VaultItemViewModel @Inject constructor(
|
|||
combine(
|
||||
vaultRepository.getVaultItemStateFlow(state.vaultItemId),
|
||||
authRepository.userStateFlow,
|
||||
) { cipherViewState, userState ->
|
||||
vaultRepository.getAuthCodeFlow(state.vaultItemId),
|
||||
) { cipherViewState, userState, authCodeState ->
|
||||
val totpCodeData = authCodeState.data?.let {
|
||||
TotpCodeItemData(
|
||||
periodSeconds = it.periodSeconds,
|
||||
timeLeftSeconds = it.timeLeftSeconds,
|
||||
totpCode = it.totpCode,
|
||||
verificationCode = it.code,
|
||||
)
|
||||
}
|
||||
|
||||
VaultItemAction.Internal.VaultDataReceive(
|
||||
userState = userState,
|
||||
vaultDataState = cipherViewState,
|
||||
vaultDataState = combineDataStates(
|
||||
cipherViewState,
|
||||
authCodeState.map { Unit },
|
||||
) { vaultData, _ ->
|
||||
VaultItemStateData(
|
||||
cipher = vaultData,
|
||||
totpCodeItemData = totpCodeData,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
|
@ -249,7 +271,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -294,6 +316,10 @@ class VaultItemViewModel @Inject constructor(
|
|||
handleCopyPasswordClick()
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.Login.CopyTotpClick -> {
|
||||
handleCopyTotpClick()
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.Login.CopyUriClick -> {
|
||||
handleCopyUriClick(action)
|
||||
}
|
||||
|
@ -342,6 +368,13 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleCopyTotpClick() {
|
||||
onLoginContent { _, login ->
|
||||
val code = login.totpCodeItemData?.verificationCode ?: return@onLoginContent
|
||||
clipboardManager.setText(text = code)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyUriClick(action: VaultItemAction.ItemType.Login.CopyUriClick) {
|
||||
clipboardManager.setText(text = action.uri)
|
||||
}
|
||||
|
@ -470,6 +503,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun handleVaultDataReceive(action: VaultItemAction.Internal.VaultDataReceive) {
|
||||
// Leave the current data alone if there is no UserState; we are in the process of logging
|
||||
// out.
|
||||
|
@ -489,8 +523,13 @@ class VaultItemViewModel @Inject constructor(
|
|||
is DataState.Loaded -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = vaultDataState.data
|
||||
?.toViewState(isPremiumUser = userState.activeAccount.isPremium)
|
||||
viewState = vaultDataState
|
||||
.data
|
||||
.cipher
|
||||
?.toViewState(
|
||||
isPremiumUser = userState.activeAccount.isPremium,
|
||||
totpCodeItemData = vaultDataState.data.totpCodeItemData,
|
||||
)
|
||||
?: VaultItemState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
|
@ -519,8 +558,13 @@ class VaultItemViewModel @Inject constructor(
|
|||
is DataState.Pending -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = vaultDataState.data
|
||||
?.toViewState(isPremiumUser = userState.activeAccount.isPremium)
|
||||
viewState = vaultDataState
|
||||
.data
|
||||
.cipher
|
||||
?.toViewState(
|
||||
isPremiumUser = userState.activeAccount.isPremium,
|
||||
totpCodeItemData = vaultDataState.data.totpCodeItemData,
|
||||
)
|
||||
?: VaultItemState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
|
@ -572,6 +616,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
DeleteCipherResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
sendEvent(VaultItemEvent.ShowToast(message = R.string.item_soft_deleted.asText()))
|
||||
|
@ -786,7 +831,7 @@ data class VaultItemState(
|
|||
* @property uris The URI associated with the login item.
|
||||
* @property passwordRevisionDate An optional string indicating the last time the
|
||||
* password was changed.
|
||||
* @property totp The optional TOTP string value to be displayed.
|
||||
* @property totpCodeItemData The optional data related the TOTP code.
|
||||
* @property isPremiumUser Indicates if the user has subscribed to a premium
|
||||
* account.
|
||||
*/
|
||||
|
@ -797,7 +842,7 @@ data class VaultItemState(
|
|||
val passwordHistoryCount: Int?,
|
||||
val uris: List<UriData>,
|
||||
val passwordRevisionDate: String?,
|
||||
val totp: String?,
|
||||
val totpCodeItemData: TotpCodeItemData?,
|
||||
val isPremiumUser: Boolean,
|
||||
) : ItemType() {
|
||||
|
||||
|
@ -1077,6 +1122,11 @@ sealed class VaultItemAction {
|
|||
*/
|
||||
data object CopyPasswordClick : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the TOTP code.
|
||||
*/
|
||||
data object CopyTotpClick : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for a URI.
|
||||
*/
|
||||
|
@ -1142,7 +1192,7 @@ sealed class VaultItemAction {
|
|||
*/
|
||||
data class VaultDataReceive(
|
||||
val userState: UserState?,
|
||||
val vaultDataState: DataState<CipherView?>,
|
||||
val vaultDataState: DataState<VaultItemStateData>,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
|
|||
data class VaultLoginItemTypeHandlers(
|
||||
val onCheckForBreachClick: () -> Unit,
|
||||
val onCopyPasswordClick: () -> Unit,
|
||||
val onCopyTotpCodeClick: () -> Unit,
|
||||
val onCopyUriClick: (String) -> Unit,
|
||||
val onCopyUsernameClick: () -> Unit,
|
||||
val onLaunchUriClick: (String) -> Unit,
|
||||
|
@ -32,6 +33,9 @@ data class VaultLoginItemTypeHandlers(
|
|||
onCopyPasswordClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
|
||||
},
|
||||
onCopyTotpCodeClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyTotpClick)
|
||||
},
|
||||
onCopyUriClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUriClick(it))
|
||||
},
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.item.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* The data relating to the verification code.
|
||||
*
|
||||
* @property periodSeconds The period for the verification code.
|
||||
* @property timeLeftSeconds The time left for the verification timer.
|
||||
* @property verificationCode The verification code for the item.
|
||||
* @property totpCode The totp code for the item.
|
||||
*/
|
||||
@Parcelize
|
||||
data class TotpCodeItemData(
|
||||
val periodSeconds: Int,
|
||||
val timeLeftSeconds: Int,
|
||||
val verificationCode: String,
|
||||
val totpCode: String,
|
||||
) : Parcelable
|
|
@ -0,0 +1,14 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.item.model
|
||||
|
||||
import com.bitwarden.core.CipherView
|
||||
|
||||
/**
|
||||
* The state containing totp code item information and the cipher for the item.
|
||||
*
|
||||
* @property cipher The cipher view for the item.
|
||||
* @property totpCodeItemData The data for the totp code.
|
||||
*/
|
||||
data class VaultItemStateData(
|
||||
val cipher: CipherView?,
|
||||
val totpCodeItemData: TotpCodeItemData?,
|
||||
)
|
|
@ -14,6 +14,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.nullIfAllEqual
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.orZeroWidthSpace
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
|
@ -29,8 +30,10 @@ private val dateTimeFormatter
|
|||
/**
|
||||
* Transforms [VaultData] into [VaultState.ViewState].
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun CipherView.toViewState(
|
||||
isPremiumUser: Boolean,
|
||||
totpCodeItemData: TotpCodeItemData?,
|
||||
): VaultItemState.ViewState =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = VaultItemState.ViewState.Content.Common(
|
||||
|
@ -58,8 +61,8 @@ fun CipherView.toViewState(
|
|||
dateTimeFormatter.format(it)
|
||||
},
|
||||
passwordHistoryCount = passwordHistory?.count(),
|
||||
totp = loginValues.totp,
|
||||
isPremiumUser = isPremiumUser,
|
||||
totpCodeItemData = totpCodeItemData,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
|
||||
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -23,13 +18,13 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenCircularCountdownIndicator
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
@ -103,7 +98,7 @@ fun VaultVerificationCodeItem(
|
|||
}
|
||||
}
|
||||
|
||||
CircularIndicator(
|
||||
BitwardenCircularCountdownIndicator(
|
||||
timeLeftSeconds = timeLeftSeconds,
|
||||
periodSeconds = periodSeconds,
|
||||
)
|
||||
|
@ -127,38 +122,6 @@ fun VaultVerificationCodeItem(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CircularIndicator(
|
||||
timeLeftSeconds: Int,
|
||||
periodSeconds: Int,
|
||||
) {
|
||||
val progressAnimate by animateFloatAsState(
|
||||
targetValue = timeLeftSeconds.toFloat() / periodSeconds,
|
||||
animationSpec = tween(
|
||||
durationMillis = periodSeconds,
|
||||
delayMillis = 0,
|
||||
easing = LinearOutSlowInEasing,
|
||||
),
|
||||
)
|
||||
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = { progressAnimate },
|
||||
modifier = Modifier.size(size = 50.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
strokeWidth = 3.dp,
|
||||
strokeCap = StrokeCap.Round,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = timeLeftSeconds.toString(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
|
|
|
@ -2388,6 +2388,66 @@ class VaultRepositoryTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getVerificationCodeFlow for a single cipher 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
|
||||
|
||||
every {
|
||||
settingsDiskSource.storeLastSyncTime(MOCK_USER_STATE.activeUserId, clock.instant())
|
||||
} just runs
|
||||
|
||||
val stateFlow = MutableStateFlow<DataState<VerificationCodeItem?>>(
|
||||
DataState.Loading,
|
||||
)
|
||||
|
||||
every {
|
||||
totpCodeManager.getTotpCodeStateFlow(userId = userId, any())
|
||||
} returns stateFlow
|
||||
|
||||
setupDataStateFlow(userId = userId)
|
||||
|
||||
vaultRepository.getAuthCodeFlow(createMockCipherView(1).id.toString()).test {
|
||||
assertEquals(
|
||||
DataState.Loading,
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
stateFlow.tryEmit(DataState.Loaded(createVerificationCodeItem()))
|
||||
|
||||
assertEquals(
|
||||
DataState.Loaded(createVerificationCodeItem()),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
vaultRepository.sync()
|
||||
|
||||
assertEquals(
|
||||
DataState.Pending(createVerificationCodeItem()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getVerificationCodesFlow should update data state when state changes`() = runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
|
|
@ -36,6 +36,7 @@ import com.x8bit.bitwarden.ui.util.isProgressBar
|
|||
import com.x8bit.bitwarden.ui.util.onFirstNodeWithTextAfterScroll
|
||||
import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll
|
||||
import com.x8bit.bitwarden.ui.util.onNodeWithTextAfterScroll
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import io.mockk.every
|
||||
|
@ -1011,8 +1012,8 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
passwordHistoryCount = null,
|
||||
uris = emptyList(),
|
||||
passwordRevisionDate = null,
|
||||
totp = null,
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -1100,6 +1101,102 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, the TOTP field should exist based on the state`() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = DEFAULT_LOGIN_VIEW_STATE.copy(
|
||||
type = DEFAULT_LOGIN.copy(
|
||||
totpCodeItemData = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(isProgressBar)
|
||||
.assertDoesNotExist()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy TOTP")
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = DEFAULT_LOGIN_VIEW_STATE.copy(
|
||||
type = DEFAULT_LOGIN.copy(
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
verificationCode = "123456",
|
||||
totpCode = "testCode",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(isProgressBar)
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy TOTP")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, TOTP item should be displayed according to state`() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = DEFAULT_LOGIN_VIEW_STATE,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(isProgressBar)
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy TOTP")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = DEFAULT_LOGIN_VIEW_STATE.copy(
|
||||
type = DEFAULT_LOGIN.copy(
|
||||
isPremiumUser = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(isProgressBar)
|
||||
.assertIsNotDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy TOTP")
|
||||
.assertIsNotDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, on copy totp click should send CopyTotpClick`() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = DEFAULT_LOGIN_VIEW_STATE,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Copy TOTP")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyTotpClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, launch uri button should be displayed according to state`() {
|
||||
val uriData = VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
|
@ -1290,8 +1387,10 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE)
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) {
|
||||
copy(totpCodeItemData = null)
|
||||
}
|
||||
}
|
||||
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
|
||||
}
|
||||
|
@ -1779,8 +1878,13 @@ private val DEFAULT_LOGIN: VaultItemState.ViewState.Content.ItemType.Login =
|
|||
),
|
||||
),
|
||||
passwordRevisionDate = "4/14/83 3:56 PM",
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
verificationCode = "123456",
|
||||
totpCode = "testCode",
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_IDENTITY: VaultItemState.ViewState.Content.ItemType.Identity =
|
||||
|
@ -1821,7 +1925,7 @@ private val EMPTY_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login =
|
|||
passwordHistoryCount = null,
|
||||
uris = emptyList(),
|
||||
passwordRevisionDate = null,
|
||||
totp = null,
|
||||
totpCodeItemData = null,
|
||||
isPremiumUser = true,
|
||||
)
|
||||
|
||||
|
|
|
@ -11,14 +11,17 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
|
|||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
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.DeleteCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.util.createCommonContent
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.util.createLoginContent
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.util.createVerificationCodeItem
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import io.mockk.coEvery
|
||||
|
@ -41,6 +44,8 @@ import org.junit.jupiter.api.Test
|
|||
class VaultItemViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableVaultItemFlow = MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
|
||||
private val mutableAuthCodeItemFlow =
|
||||
MutableStateFlow<DataState<VerificationCodeItem?>>(DataState.Loading)
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
|
||||
private val clipboardManager: BitwardenClipboardManager = mockk()
|
||||
|
@ -48,6 +53,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
private val vaultRepo: VaultRepository = mockk {
|
||||
every { getAuthCodeFlow(VAULT_ITEM_ID) } returns mutableAuthCodeItemFlow
|
||||
every { getVaultItemStateFlow(VAULT_ITEM_ID) } returns mutableVaultItemFlow
|
||||
}
|
||||
|
||||
|
@ -73,6 +79,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
every {
|
||||
vaultRepo.getVaultItemStateFlow(differentVaultItemId)
|
||||
} returns MutableStateFlow<DataState<CipherView?>>(DataState.Loading)
|
||||
|
||||
every {
|
||||
vaultRepo.getAuthCodeFlow(differentVaultItemId)
|
||||
} returns MutableStateFlow<DataState<VerificationCodeItem?>>(DataState.Loading)
|
||||
|
||||
val state = DEFAULT_STATE.copy(vaultItemId = differentVaultItemId)
|
||||
val viewModel = createViewModel(state = state)
|
||||
assertEquals(state, viewModel.stateFlow.value)
|
||||
|
@ -117,9 +128,17 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
fun `ConfirmDeleteClick with DeleteCipherResult Success should should ShowToast and NavigateBack`() =
|
||||
runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
coEvery {
|
||||
vaultRepo.softDeleteCipher(
|
||||
|
@ -147,9 +166,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
fun `ConfirmDeleteClick with DeleteCipherResult Failure should should Show generic error`() =
|
||||
runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
coEvery {
|
||||
vaultRepo.softDeleteCipher(
|
||||
|
@ -176,9 +202,17 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
fun `ConfirmRestoreClick with RestoreCipherResult Success should should ShowToast and NavigateBack`() =
|
||||
runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
coEvery {
|
||||
vaultRepo.restoreCipher(
|
||||
|
@ -206,9 +240,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
fun `ConfirmRestoreClick with RestoreCipherResult Failure should should Show generic error`() =
|
||||
runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
coEvery {
|
||||
vaultRepo.restoreCipher(
|
||||
|
@ -246,9 +287,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `on EditClick should prompt for master password when required`() = runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem())
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
|
@ -266,9 +313,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
|
||||
|
@ -290,9 +343,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
|
||||
|
@ -336,9 +396,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick("field"))
|
||||
|
@ -348,7 +414,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,17 +427,25 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
val field = "field"
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false))
|
||||
}
|
||||
every { clipboardManager.setText(text = field) } just runs
|
||||
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick(field))
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = field)
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -390,9 +467,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
|
@ -412,7 +495,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -437,9 +523,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
|
@ -461,7 +553,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -531,9 +626,17 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `on CheckForBreachClick should process a password`() = runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val breachCount = 5
|
||||
coEvery {
|
||||
|
@ -562,7 +665,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
authRepo.getPasswordBreachCount(password = DEFAULT_LOGIN_PASSWORD)
|
||||
|
@ -574,9 +680,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
|
||||
|
@ -586,26 +699,55 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on CopyPasswordClick should call setText on the CLipboardManager when re-prompt is not required`() {
|
||||
fun `on CopyPasswordClick should call setText on the ClipboardManager when re-prompt is not required`() {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false))
|
||||
}
|
||||
every { clipboardManager.setText(text = DEFAULT_LOGIN_PASSWORD) } just runs
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
every { clipboardManager.setText(text = DEFAULT_LOGIN_PASSWORD) } just runs
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = DEFAULT_LOGIN_PASSWORD)
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyTotpClick should call setText on the ClipboardManager`() {
|
||||
every { clipboardManager.setText(text = "123456") } just runs
|
||||
mutableVaultItemFlow.value = DataState.Loaded(
|
||||
data = createMockCipherView(1),
|
||||
)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(
|
||||
data = createVerificationCodeItem(),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyTotpClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = "123456")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -624,9 +766,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick)
|
||||
|
@ -636,7 +785,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -645,17 +797,24 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
fun `on CopyUsernameClick should call setText on ClipboardManager when re-prompt is not required`() {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns createViewState(common = DEFAULT_COMMON.copy(requiresReprompt = false))
|
||||
}
|
||||
every { clipboardManager.setText(text = DEFAULT_LOGIN_USERNAME) } just runs
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = DEFAULT_LOGIN_USERNAME)
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -673,9 +832,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick)
|
||||
|
@ -685,7 +851,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -695,7 +864,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
.returns(
|
||||
createViewState(
|
||||
|
@ -704,6 +876,8 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick)
|
||||
|
@ -714,7 +888,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -724,9 +901,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
|
@ -740,7 +924,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -753,9 +940,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
} returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value =
|
||||
DataState.Loaded(data = createVerificationCodeItem())
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
|
@ -776,7 +970,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = createTotpCodeData(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -799,9 +996,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns CARD_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns CARD_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(cardState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick)
|
||||
|
@ -811,7 +1014,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -820,7 +1026,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
fun `on CopyNumberClick should call setText on the ClipboardManager when re-prompt is not required`() {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
type = DEFAULT_CARD_TYPE,
|
||||
|
@ -828,12 +1037,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
every { clipboardManager.setText(text = "12345436") } just runs
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopyNumberClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = "12345436")
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -842,9 +1055,15 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val cardState = DEFAULT_STATE.copy(viewState = CARD_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns CARD_VIEW_STATE
|
||||
every {
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns CARD_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
assertEquals(cardState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
||||
|
@ -854,7 +1073,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -863,7 +1085,10 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
fun `on CopySecurityCodeClick should call setText on the ClipboardManager when re-prompt is not required`() {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
} returns createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
type = DEFAULT_CARD_TYPE,
|
||||
|
@ -871,12 +1096,16 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
every { clipboardManager.setText(text = "987") } just runs
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Card.CopySecurityCodeClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(text = "987")
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
mockCipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -906,6 +1135,14 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
type = type,
|
||||
)
|
||||
|
||||
private fun createTotpCodeData() =
|
||||
TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 30,
|
||||
verificationCode = "123456",
|
||||
totpCode = "mockTotp-1",
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val VAULT_ITEM_ID = "vault_item_id"
|
||||
private const val DEFAULT_LOGIN_PASSWORD = "password"
|
||||
|
@ -951,9 +1188,14 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
),
|
||||
),
|
||||
passwordRevisionDate = "12/31/69 06:16 PM",
|
||||
totp = "otpauth://totp/Example:alice@google.com" +
|
||||
"?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
totpCode = "otpauth://totp/Example:alice@google.com" +
|
||||
"?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
verificationCode = "123456",
|
||||
timeLeftSeconds = 15,
|
||||
periodSeconds = 30,
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_CARD_TYPE: VaultItemState.ViewState.Content.ItemType.Card =
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.item.util
|
|||
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
@ -25,7 +26,15 @@ class CipherViewExtensionsTest {
|
|||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState Login Content with premium`() {
|
||||
val cipherView = createCipherView(type = CipherType.LOGIN, isEmpty = false)
|
||||
val viewState = cipherView.toViewState(isPremiumUser = true)
|
||||
val viewState = cipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
verificationCode = "123456",
|
||||
totpCode = "testCode",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
|
@ -41,7 +50,15 @@ class CipherViewExtensionsTest {
|
|||
fun `toViewState should transform full CipherView into ViewState Login Content without premium`() {
|
||||
val isPremiumUser = false
|
||||
val cipherView = createCipherView(type = CipherType.LOGIN, isEmpty = false)
|
||||
val viewState = cipherView.toViewState(isPremiumUser = isPremiumUser)
|
||||
val viewState = cipherView.toViewState(
|
||||
isPremiumUser = isPremiumUser,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
verificationCode = "123456",
|
||||
totpCode = "testCode",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
|
@ -55,7 +72,10 @@ class CipherViewExtensionsTest {
|
|||
@Test
|
||||
fun `toViewState should transform empty CipherView into ViewState Login Content`() {
|
||||
val cipherView = createCipherView(type = CipherType.LOGIN, isEmpty = true)
|
||||
val viewState = cipherView.toViewState(isPremiumUser = true)
|
||||
val viewState = cipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
|
@ -71,7 +91,10 @@ class CipherViewExtensionsTest {
|
|||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState Identity Content`() {
|
||||
val cipherView = createCipherView(type = CipherType.IDENTITY, isEmpty = false)
|
||||
val viewState = cipherView.toViewState(isPremiumUser = true)
|
||||
val viewState = cipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
|
@ -85,7 +108,10 @@ class CipherViewExtensionsTest {
|
|||
@Test
|
||||
fun `toViewState should transform empty CipherView into ViewState Identity Content`() {
|
||||
val cipherView = createCipherView(type = CipherType.IDENTITY, isEmpty = true)
|
||||
val viewState = cipherView.toViewState(isPremiumUser = true)
|
||||
val viewState = cipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
|
@ -109,7 +135,10 @@ class CipherViewExtensionsTest {
|
|||
lastName = null,
|
||||
),
|
||||
)
|
||||
val viewState = cipherView.toViewState(isPremiumUser = true)
|
||||
val viewState = cipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
|
@ -138,7 +167,10 @@ class CipherViewExtensionsTest {
|
|||
country = null,
|
||||
),
|
||||
)
|
||||
val result = cipherView.toViewState(isPremiumUser = true)
|
||||
val result = cipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
|
@ -170,7 +202,10 @@ class CipherViewExtensionsTest {
|
|||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState Secure Note Content`() {
|
||||
val cipherView = createCipherView(type = CipherType.SECURE_NOTE, isEmpty = false)
|
||||
val viewState = cipherView.toViewState(isPremiumUser = true)
|
||||
val viewState = cipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
|
@ -185,7 +220,10 @@ class CipherViewExtensionsTest {
|
|||
@Test
|
||||
fun `toViewState should transform empty Secure Note CipherView into ViewState Secure Note Content`() {
|
||||
val cipherView = createCipherView(type = CipherType.SECURE_NOTE, isEmpty = true)
|
||||
val viewState = cipherView.toViewState(isPremiumUser = true)
|
||||
val viewState = cipherView.toViewState(
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
)
|
||||
|
||||
val expectedState = VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(isEmpty = true).copy(currentCipher = cipherView),
|
||||
|
|
|
@ -10,6 +10,7 @@ import com.bitwarden.core.LoginUriView
|
|||
import com.bitwarden.core.LoginView
|
||||
import com.bitwarden.core.PasswordHistoryView
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import java.time.Instant
|
||||
|
||||
|
@ -196,9 +197,14 @@ fun createLoginContent(isEmpty: Boolean): VaultItemState.ViewState.Content.ItemT
|
|||
)
|
||||
},
|
||||
passwordRevisionDate = "1/1/70 12:16 AM".takeUnless { isEmpty },
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"
|
||||
.takeUnless { isEmpty },
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = TotpCodeItemData(
|
||||
periodSeconds = 30,
|
||||
timeLeftSeconds = 15,
|
||||
verificationCode = "123456",
|
||||
totpCode = "testCode",
|
||||
)
|
||||
.takeUnless { isEmpty },
|
||||
)
|
||||
|
||||
fun createIdentityContent(
|
||||
|
|
Loading…
Add table
Reference in a new issue