mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
BIT-1321, BIT-1014: Implement Verify PIN screen (#635)
This commit is contained in:
parent
ca517c88c4
commit
880bdc8826
16 changed files with 612 additions and 106 deletions
|
@ -25,6 +25,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserState
|
||||
|
@ -113,6 +114,7 @@ class AuthRepositoryImpl constructor(
|
|||
vaultState = vaultState,
|
||||
userOrganizationsList = userOrganizationsList,
|
||||
specialCircumstance = specialCircumstance,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
)
|
||||
}
|
||||
.stateIn(
|
||||
|
@ -124,6 +126,7 @@ class AuthRepositoryImpl constructor(
|
|||
vaultState = vaultRepository.vaultStateFlow.value,
|
||||
userOrganizationsList = authDiskSource.userOrganizationsList,
|
||||
specialCircumstance = mutableSpecialCircumstanceStateFlow.value,
|
||||
vaultUnlockTypeProvider = ::getVaultUnlockType,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -401,4 +404,17 @@ class AuthRepositoryImpl constructor(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getVaultUnlockType(
|
||||
userId: String,
|
||||
): VaultUnlockType =
|
||||
when {
|
||||
authDiskSource.getPinProtectedUserKey(userId = userId) != null -> {
|
||||
VaultUnlockType.PIN
|
||||
}
|
||||
|
||||
else -> {
|
||||
VaultUnlockType.MASTER_PASSWORD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ data class UserState(
|
|||
* authentication to view their vault.
|
||||
* @property isVaultUnlocked Whether or not the user's vault is currently unlocked.
|
||||
* @property organizations List of [Organization]s the user is associated with, if any.
|
||||
* @property vaultUnlockType The mechanism by which the user's vault may be unlocked.
|
||||
*/
|
||||
data class Account(
|
||||
val userId: String,
|
||||
|
@ -57,6 +58,7 @@ data class UserState(
|
|||
val isLoggedIn: Boolean,
|
||||
val isVaultUnlocked: Boolean,
|
||||
val organizations: List<Organization>,
|
||||
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* The mechanism by which the user's vault may be unlocked.
|
||||
*/
|
||||
enum class VaultUnlockType {
|
||||
/**
|
||||
* The vault must be unlocked using a master password.
|
||||
*/
|
||||
MASTER_PASSWORD,
|
||||
|
||||
/**
|
||||
* The vault must be unlocked using a PIN.
|
||||
*/
|
||||
PIN,
|
||||
}
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository.util
|
|||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
|
@ -47,6 +48,7 @@ fun UserStateJson.toUserState(
|
|||
vaultState: VaultState,
|
||||
userOrganizationsList: List<UserOrganizations>,
|
||||
specialCircumstance: UserState.SpecialCircumstance?,
|
||||
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
|
||||
): UserState =
|
||||
UserState(
|
||||
activeUserId = this.activeUserId,
|
||||
|
@ -72,6 +74,7 @@ fun UserStateJson.toUserState(
|
|||
.find { it.userId == userId }
|
||||
?.organizations
|
||||
.orEmpty(),
|
||||
vaultUnlockType = vaultUnlockTypeProvider(userId),
|
||||
)
|
||||
},
|
||||
specialCircumstance = specialCircumstance,
|
||||
|
|
|
@ -300,6 +300,7 @@ class VaultRepositoryImpl(
|
|||
.also {
|
||||
if (it is VaultUnlockResult.Success) {
|
||||
sync()
|
||||
deriveTemporaryPinProtectedUserKeyIfNecessary(userId = userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -517,6 +518,33 @@ class VaultRepositoryImpl(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
|
||||
* key. This indicates a scenario in which a user has requested PIN unlocking but requires
|
||||
* master-password unlocking on app restart. This function may then be called after such an
|
||||
* unlock to derive a pin-protected user key and store it in memory for use for any subsequent
|
||||
* unlocks during this current app session.
|
||||
*
|
||||
* If the user's vault has not yet been unlocked, this call will do nothing.
|
||||
*/
|
||||
private suspend fun deriveTemporaryPinProtectedUserKeyIfNecessary(userId: String) {
|
||||
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId) ?: return
|
||||
val existingPinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
|
||||
if (existingPinProtectedUserKey != null) return
|
||||
vaultSdkSource
|
||||
.derivePinProtectedUserKey(
|
||||
userId = userId,
|
||||
encryptedPin = encryptedPin,
|
||||
)
|
||||
.onSuccess { pinProtectedUserKey ->
|
||||
authDiskSource.storePinProtectedUserKey(
|
||||
userId = userId,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
inMemoryOnly = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun storeProfileData(
|
||||
syncResponse: SyncResponseJson,
|
||||
) {
|
||||
|
|
|
@ -30,6 +30,10 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenInputLabel
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenKeyboardType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenMessage
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenTitle
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
|
@ -116,7 +120,7 @@ fun VaultUnlockScreen(
|
|||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.verify_master_password),
|
||||
title = state.vaultUnlockType.unlockScreenTitle(),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = null,
|
||||
actions = {
|
||||
|
@ -145,18 +149,19 @@ fun VaultUnlockScreen(
|
|||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
value = state.passwordInput,
|
||||
label = state.vaultUnlockType.unlockScreenInputLabel(),
|
||||
value = state.input,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultUnlockAction.PasswordInputChanged(it)) }
|
||||
{ viewModel.trySendAction(VaultUnlockAction.InputChanged(it)) }
|
||||
},
|
||||
keyboardType = state.vaultUnlockType.unlockScreenKeyboardType,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.vault_locked_master_password),
|
||||
text = state.vaultUnlockType.unlockScreenMessage(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
|
@ -182,7 +187,7 @@ fun VaultUnlockScreen(
|
|||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
|
||||
},
|
||||
isEnabled = state.passwordInput.isNotEmpty(),
|
||||
isEnabled = state.input.isNotEmpty(),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
|
|
|
@ -7,9 +7,11 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenErrorMessage
|
||||
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
|
||||
|
@ -50,7 +52,8 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
email = activeAccountSummary.email,
|
||||
dialog = null,
|
||||
environmentUrl = environmentRepo.environment.label,
|
||||
passwordInput = "",
|
||||
input = "",
|
||||
vaultUnlockType = userState.activeAccount.vaultUnlockType,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -79,7 +82,7 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
VaultUnlockAction.AddAccountClick -> handleAddAccountClick()
|
||||
VaultUnlockAction.DismissDialog -> handleDismissDialog()
|
||||
VaultUnlockAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
|
||||
is VaultUnlockAction.PasswordInputChanged -> handlePasswordInputChanged(action)
|
||||
is VaultUnlockAction.InputChanged -> handleInputChanged(action)
|
||||
is VaultUnlockAction.LockAccountClick -> handleLockAccountClick(action)
|
||||
is VaultUnlockAction.LogoutAccountClick -> handleLogoutAccountClick(action)
|
||||
is VaultUnlockAction.SwitchAccountClick -> handleSwitchAccountClick(action)
|
||||
|
@ -106,9 +109,9 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
authRepository.logout()
|
||||
}
|
||||
|
||||
private fun handlePasswordInputChanged(action: VaultUnlockAction.PasswordInputChanged) {
|
||||
private fun handleInputChanged(action: VaultUnlockAction.InputChanged) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(passwordInput = action.passwordInput)
|
||||
it.copy(input = action.input)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,9 +130,19 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
private fun handleUnlockClick() {
|
||||
mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) }
|
||||
viewModelScope.launch {
|
||||
val vaultUnlockResult = vaultRepo.unlockVaultWithMasterPasswordAndSync(
|
||||
mutableStateFlow.value.passwordInput,
|
||||
)
|
||||
val vaultUnlockResult = when (state.vaultUnlockType) {
|
||||
VaultUnlockType.MASTER_PASSWORD -> {
|
||||
vaultRepo.unlockVaultWithMasterPasswordAndSync(
|
||||
mutableStateFlow.value.input,
|
||||
)
|
||||
}
|
||||
|
||||
VaultUnlockType.PIN -> {
|
||||
vaultRepo.unlockVaultWithPinAndSync(
|
||||
mutableStateFlow.value.input,
|
||||
)
|
||||
}
|
||||
}
|
||||
sendAction(VaultUnlockAction.Internal.ReceiveVaultUnlockResult(vaultUnlockResult))
|
||||
}
|
||||
}
|
||||
|
@ -142,7 +155,7 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||
R.string.invalid_master_password.asText(),
|
||||
state.vaultUnlockType.unlockScreenErrorMessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -185,6 +198,7 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||
accountSummaries = accountSummaries,
|
||||
email = activeAccountSummary.email,
|
||||
vaultUnlockType = userState.activeAccount.vaultUnlockType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -201,7 +215,8 @@ data class VaultUnlockState(
|
|||
val email: String,
|
||||
val environmentUrl: String,
|
||||
val dialog: VaultUnlockDialog?,
|
||||
val passwordInput: String,
|
||||
val input: String,
|
||||
val vaultUnlockType: VaultUnlockType,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
|
@ -261,10 +276,10 @@ sealed class VaultUnlockAction {
|
|||
data object ConfirmLogoutClick : VaultUnlockAction()
|
||||
|
||||
/**
|
||||
* The user has modified the password input.
|
||||
* The user has modified the input.
|
||||
*/
|
||||
data class PasswordInputChanged(
|
||||
val passwordInput: String,
|
||||
data class InputChanged(
|
||||
val input: String,
|
||||
) : VaultUnlockAction()
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util
|
||||
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
||||
/**
|
||||
* A title to use on the Vault Unlock screen.
|
||||
*/
|
||||
val VaultUnlockType.unlockScreenTitle: Text
|
||||
get() = when (this) {
|
||||
VaultUnlockType.MASTER_PASSWORD -> R.string.verify_master_password
|
||||
VaultUnlockType.PIN -> R.string.verify_pin
|
||||
}
|
||||
.asText()
|
||||
|
||||
/**
|
||||
* A descriptive message to use on the Vault Unlock screen.
|
||||
*/
|
||||
val VaultUnlockType.unlockScreenMessage: Text
|
||||
get() = when (this) {
|
||||
VaultUnlockType.MASTER_PASSWORD -> R.string.vault_locked_master_password
|
||||
VaultUnlockType.PIN -> R.string.vault_locked_pin
|
||||
}
|
||||
.asText()
|
||||
|
||||
/**
|
||||
* The label for the main text input to use on the Vault Unlock screen.
|
||||
*/
|
||||
val VaultUnlockType.unlockScreenInputLabel: Text
|
||||
get() = when (this) {
|
||||
VaultUnlockType.MASTER_PASSWORD -> R.string.master_password
|
||||
VaultUnlockType.PIN -> R.string.pin
|
||||
}
|
||||
.asText()
|
||||
|
||||
/**
|
||||
* The error message to use for a failed unlock on the Vault Unlock screen.
|
||||
*/
|
||||
val VaultUnlockType.unlockScreenErrorMessage: Text
|
||||
get() = when (this) {
|
||||
VaultUnlockType.MASTER_PASSWORD -> R.string.invalid_master_password
|
||||
VaultUnlockType.PIN -> R.string.invalid_pin
|
||||
}
|
||||
.asText()
|
||||
|
||||
/**
|
||||
* The [KeyboardType] to use on the input on the Vault Unlock screen.
|
||||
*/
|
||||
val VaultUnlockType.unlockScreenKeyboardType: KeyboardType
|
||||
get() = when (this) {
|
||||
VaultUnlockType.MASTER_PASSWORD -> KeyboardType.Password
|
||||
VaultUnlockType.PIN -> KeyboardType.Number
|
||||
}
|
|
@ -191,12 +191,10 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
it.copy(isUnlockWithPinEnabled = action.isUnlockWithPinEnabled)
|
||||
}
|
||||
|
||||
// TODO: Complete implementation (BIT-465)
|
||||
when (action) {
|
||||
AccountSecurityAction.UnlockWithPinToggle.PendingEnabled -> Unit
|
||||
AccountSecurityAction.UnlockWithPinToggle.Disabled -> {
|
||||
settingsRepository.clearUnlockPin()
|
||||
sendEvent(AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()))
|
||||
}
|
||||
|
||||
is AccountSecurityAction.UnlockWithPinToggle.Enabled -> {
|
||||
|
@ -205,7 +203,6 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
shouldRequireMasterPasswordOnRestart =
|
||||
action.shouldRequireMasterPasswordOnRestart,
|
||||
)
|
||||
sendEvent(AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
|
@ -201,7 +202,7 @@ class AuthRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `userStateFlow should update according to changes in its underyling data sources`() {
|
||||
fun `userStateFlow should update according to changes in its underlying data sources`() {
|
||||
fakeAuthDiskSource.userState = null
|
||||
assertEquals(
|
||||
null,
|
||||
|
@ -215,16 +216,28 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.userState = MULTI_USER_STATE
|
||||
fakeAuthDiskSource.apply {
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_1,
|
||||
pinProtectedUserKey = "pinProtectedUseKey",
|
||||
)
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_2,
|
||||
pinProtectedUserKey = "pinProtectedUseKey",
|
||||
)
|
||||
userState = MULTI_USER_STATE
|
||||
}
|
||||
assertEquals(
|
||||
MULTI_USER_STATE.toUserState(
|
||||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
@ -239,19 +252,31 @@ class AuthRepositoryTest {
|
|||
vaultState = emptyVaultState,
|
||||
userOrganizationsList = emptyList(),
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.storeOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = ORGANIZATIONS,
|
||||
)
|
||||
fakeAuthDiskSource.apply {
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_1,
|
||||
pinProtectedUserKey = null,
|
||||
)
|
||||
storePinProtectedUserKey(
|
||||
userId = USER_ID_2,
|
||||
pinProtectedUserKey = null,
|
||||
)
|
||||
storeOrganizations(
|
||||
userId = USER_ID_1,
|
||||
organizations = ORGANIZATIONS,
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
MULTI_USER_STATE.toUserState(
|
||||
vaultState = emptyVaultState,
|
||||
userOrganizationsList = USER_ORGANIZATIONS,
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
),
|
||||
repository.userStateFlow.value,
|
||||
)
|
||||
|
@ -280,6 +305,7 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
)
|
||||
mutableVaultStateFlow.value = VAULT_STATE
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
@ -1054,6 +1080,7 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
assertEquals(
|
||||
|
@ -1084,6 +1111,7 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
assertEquals(
|
||||
|
@ -1112,6 +1140,7 @@ class AuthRepositoryTest {
|
|||
vaultState = VAULT_STATE,
|
||||
userOrganizationsList = emptyList(),
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
)
|
||||
fakeAuthDiskSource.userState = MULTI_USER_STATE
|
||||
assertEquals(
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
|
||||
import io.mockk.every
|
||||
|
@ -112,6 +113,7 @@ class UserStateJsonExtensionsTest {
|
|||
name = "organizationName",
|
||||
),
|
||||
),
|
||||
vaultUnlockType = VaultUnlockType.PIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -153,6 +155,7 @@ class UserStateJsonExtensionsTest {
|
|||
),
|
||||
),
|
||||
specialCircumstance = null,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -179,6 +182,7 @@ class UserStateJsonExtensionsTest {
|
|||
name = "organizationName",
|
||||
),
|
||||
),
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
),
|
||||
),
|
||||
specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition,
|
||||
|
@ -221,6 +225,7 @@ class UserStateJsonExtensionsTest {
|
|||
),
|
||||
),
|
||||
specialCircumstance = UserState.SpecialCircumstance.PendingAccountAddition,
|
||||
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -651,11 +651,25 @@ class VaultRepositoryTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithMasterPasswordAndSync with VaultLockManager Success should unlock for the current user, sync, and return Success`() =
|
||||
fun `unlockVaultWithMasterPasswordAndSync with VaultLockManager Success and no encrypted PIN should unlock for the current user, sync, and return Success`() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
val mockVaultUnlockResult = VaultUnlockResult.Success
|
||||
coEvery {
|
||||
vaultSdkSource.derivePinProtectedUserKey(any(), any())
|
||||
} returns "pinProtectedUserKey".asSuccess()
|
||||
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
|
||||
fakeAuthDiskSource.apply {
|
||||
storeEncryptedPin(
|
||||
userId = userId,
|
||||
encryptedPin = null,
|
||||
)
|
||||
storePinProtectedUserKey(
|
||||
userId = userId,
|
||||
pinProtectedUserKey = null,
|
||||
isInMemoryOnly = true,
|
||||
)
|
||||
}
|
||||
|
||||
val result = vaultRepository.unlockVaultWithMasterPasswordAndSync(
|
||||
masterPassword = "mockPassword-1",
|
||||
|
@ -680,6 +694,69 @@ class VaultRepositoryTest {
|
|||
organizationKeys = createMockOrganizationKeys(number = 1),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { vaultSdkSource.derivePinProtectedUserKey(any(), any()) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVaultWithMasterPasswordAndSync with VaultLockManager Success and a stored encrypted pin should unlock for the current user, sync, derive a new pin-protected key, and return Success`() =
|
||||
runTest {
|
||||
val userId = "mockId-1"
|
||||
val encryptedPin = "encryptedPin"
|
||||
val pinProtectedUserKey = "pinProtectedUserkey"
|
||||
val mockVaultUnlockResult = VaultUnlockResult.Success
|
||||
coEvery {
|
||||
vaultSdkSource.derivePinProtectedUserKey(
|
||||
userId = userId,
|
||||
encryptedPin = encryptedPin,
|
||||
)
|
||||
} returns pinProtectedUserKey.asSuccess()
|
||||
prepareStateForUnlocking(unlockResult = mockVaultUnlockResult)
|
||||
fakeAuthDiskSource.apply {
|
||||
storeEncryptedPin(
|
||||
userId = userId,
|
||||
encryptedPin = encryptedPin,
|
||||
)
|
||||
storePinProtectedUserKey(
|
||||
userId = userId,
|
||||
pinProtectedUserKey = null,
|
||||
isInMemoryOnly = true,
|
||||
)
|
||||
}
|
||||
|
||||
val result = vaultRepository.unlockVaultWithMasterPasswordAndSync(
|
||||
masterPassword = "mockPassword-1",
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
mockVaultUnlockResult,
|
||||
result,
|
||||
)
|
||||
fakeAuthDiskSource.assertPinProtectedUserKey(
|
||||
userId = userId,
|
||||
pinProtectedUserKey = pinProtectedUserKey,
|
||||
inMemoryOnly = true,
|
||||
)
|
||||
coVerify { syncService.sync() }
|
||||
coVerify {
|
||||
vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
kdf = MOCK_PROFILE.toSdkParams(),
|
||||
email = "email",
|
||||
privateKey = "mockPrivateKey-1",
|
||||
initUserCryptoMethod = InitUserCryptoMethod.Password(
|
||||
password = "mockPassword-1",
|
||||
userKey = "mockKey-1",
|
||||
),
|
||||
organizationKeys = createMockOrganizationKeys(number = 1),
|
||||
)
|
||||
}
|
||||
coEvery {
|
||||
vaultSdkSource.derivePinProtectedUserKey(
|
||||
userId = userId,
|
||||
encryptedPin = encryptedPin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
|
|
@ -13,6 +13,7 @@ import androidx.compose.ui.test.onNodeWithText
|
|||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
|
@ -113,6 +114,81 @@ class VaultUnlockScreenTest : BaseComposeTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `title should change according to state`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(vaultUnlockType = VaultUnlockType.MASTER_PASSWORD)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Verify master password")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("Verify PIN")
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(vaultUnlockType = VaultUnlockType.PIN)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Verify master password")
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onNodeWithText("Verify PIN")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `message should change according to state`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(vaultUnlockType = VaultUnlockType.MASTER_PASSWORD)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Your vault is locked. Verify your master password to continue.")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("Your vault is locked. Verify your PIN code to continue.")
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(vaultUnlockType = VaultUnlockType.PIN)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Your vault is locked. Verify your master password to continue.")
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onNodeWithText("Your vault is locked. Verify your PIN code to continue.")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `input label should change according to state`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(vaultUnlockType = VaultUnlockType.MASTER_PASSWORD)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Master password")
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("PIN")
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(vaultUnlockType = VaultUnlockType.PIN)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Master password")
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onNodeWithText("PIN")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `lock button click in the lock-or-logout dialog should send LockAccountClick action and close the dialog`() {
|
||||
|
@ -235,15 +311,15 @@ class VaultUnlockScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `password input state change should update unlock button enabled`() {
|
||||
fun `input state change should update unlock button enabled`() {
|
||||
composeTestRule.onNodeWithText("Unlock").performScrollTo().assertIsNotEnabled()
|
||||
mutableStateFlow.update { it.copy(passwordInput = "a") }
|
||||
mutableStateFlow.update { it.copy(input = "a") }
|
||||
composeTestRule.onNodeWithText("Unlock").performScrollTo().assertIsEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unlock click should send UnlockClick action`() {
|
||||
mutableStateFlow.update { it.copy(passwordInput = "abdc1234") }
|
||||
mutableStateFlow.update { it.copy(input = "abdc1234") }
|
||||
composeTestRule
|
||||
.onNodeWithText("Unlock")
|
||||
.performScrollTo()
|
||||
|
@ -252,14 +328,14 @@ class VaultUnlockScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `master password change should send PasswordInputChanged action`() {
|
||||
fun `input change should send InputChanged action`() {
|
||||
val input = "abcd1234"
|
||||
composeTestRule
|
||||
.onNodeWithText("Master password")
|
||||
.performScrollTo()
|
||||
.performTextInput(input)
|
||||
verify {
|
||||
viewModel.trySendAction(VaultUnlockAction.PasswordInputChanged(input))
|
||||
viewModel.trySendAction(VaultUnlockAction.InputChanged(input))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -300,5 +376,6 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
|
|||
email = "bit@bitwarden.com",
|
||||
environmentUrl = DEFAULT_ENVIRONMENT_URL,
|
||||
initials = "AU",
|
||||
passwordInput = "",
|
||||
input = "",
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
|
@ -52,7 +53,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `initial state should be correct when set`() {
|
||||
val state = DEFAULT_STATE.copy(
|
||||
passwordInput = "pass",
|
||||
input = "pass",
|
||||
)
|
||||
val viewModel = createViewModel(state = state)
|
||||
assertEquals(state, viewModel.stateFlow.value)
|
||||
|
@ -199,9 +200,9 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
fun `on PasswordInputChanged should update the password input state`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val password = "abcd1234"
|
||||
viewModel.trySendAction(VaultUnlockAction.PasswordInputChanged(passwordInput = password))
|
||||
viewModel.trySendAction(VaultUnlockAction.InputChanged(input = password))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(passwordInput = password),
|
||||
DEFAULT_STATE.copy(input = password),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
@ -247,9 +248,12 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick should display error dialog on AuthenticationError`() = runTest {
|
||||
fun `on UnlockClick for password unlock should display error dialog on AuthenticationError`() {
|
||||
val password = "abcd1234"
|
||||
val initialState = DEFAULT_STATE.copy(passwordInput = password)
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = password,
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
|
||||
|
@ -270,9 +274,12 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick should display error dialog on GenericError`() = runTest {
|
||||
fun `on UnlockClick for password unlock should display error dialog on GenericError`() {
|
||||
val password = "abcd1234"
|
||||
val initialState = DEFAULT_STATE.copy(passwordInput = password)
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = password,
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
|
||||
|
@ -293,9 +300,12 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick should display error dialog on InvalidStateError`() = runTest {
|
||||
fun `on UnlockClick for password unlock should display error dialog on InvalidStateError`() {
|
||||
val password = "abcd1234"
|
||||
val initialState = DEFAULT_STATE.copy(passwordInput = password)
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = password,
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
|
||||
|
@ -316,9 +326,12 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick should display clear dialog on success`() = runTest {
|
||||
fun `on UnlockClick for password unlock should clear dialog on success`() {
|
||||
val password = "abcd1234"
|
||||
val initialState = DEFAULT_STATE.copy(passwordInput = password)
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = password,
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
|
||||
|
@ -334,6 +347,106 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for PIN unlock should display error dialog on AuthenticationError`() {
|
||||
val pin = "1234"
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = pin,
|
||||
vaultUnlockType = VaultUnlockType.PIN,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
} returns VaultUnlockResult.AuthenticationError
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||
R.string.invalid_pin.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for PIN unlock should display error dialog on GenericError`() {
|
||||
val pin = "1234"
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = pin,
|
||||
vaultUnlockType = VaultUnlockType.PIN,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
} returns VaultUnlockResult.GenericError
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||
R.string.generic_error_message.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for PIN unlock should display error dialog on InvalidStateError`() {
|
||||
val pin = "1234"
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = pin,
|
||||
vaultUnlockType = VaultUnlockType.PIN,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
} returns VaultUnlockResult.InvalidStateError
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||
R.string.generic_error_message.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for PIN unlock should clear dialog on success`() {
|
||||
val pin = "1234"
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = pin,
|
||||
vaultUnlockType = VaultUnlockType.PIN,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
} returns VaultUnlockResult.Success
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
initialState.copy(dialog = null),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: VaultUnlockState? = DEFAULT_STATE,
|
||||
environmentRepo: EnvironmentRepository = environmentRepository,
|
||||
|
@ -364,7 +477,8 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
|
|||
initials = "AU",
|
||||
dialog = null,
|
||||
environmentUrl = Environment.Us.label,
|
||||
passwordInput = "",
|
||||
input = "",
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
|
||||
private val DEFAULT_USER_STATE = UserState(
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util
|
||||
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VaultUnlockTypeExtensionsTest {
|
||||
@Test
|
||||
fun `unlockScreenTitle should return the correct title for each type`() {
|
||||
mapOf(
|
||||
VaultUnlockType.MASTER_PASSWORD to R.string.verify_master_password.asText(),
|
||||
VaultUnlockType.PIN to R.string.verify_pin.asText(),
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
assertEquals(
|
||||
expected,
|
||||
type.unlockScreenTitle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unlockScreenMessage should return the correct title for each type`() {
|
||||
mapOf(
|
||||
VaultUnlockType.MASTER_PASSWORD to R.string.vault_locked_master_password.asText(),
|
||||
VaultUnlockType.PIN to R.string.vault_locked_pin.asText(),
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
assertEquals(
|
||||
expected,
|
||||
type.unlockScreenMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unlockScreenInputLabel should return the correct title for each type`() {
|
||||
mapOf(
|
||||
VaultUnlockType.MASTER_PASSWORD to R.string.master_password.asText(),
|
||||
VaultUnlockType.PIN to R.string.pin.asText(),
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
assertEquals(
|
||||
expected,
|
||||
type.unlockScreenInputLabel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unlockScreenErrorMessage should return the correct title for each type`() {
|
||||
mapOf(
|
||||
VaultUnlockType.MASTER_PASSWORD to R.string.invalid_master_password.asText(),
|
||||
VaultUnlockType.PIN to R.string.invalid_pin.asText(),
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
assertEquals(
|
||||
expected,
|
||||
type.unlockScreenErrorMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unlockScreenKeyboardType should return the correct title for each type`() {
|
||||
mapOf(
|
||||
VaultUnlockType.MASTER_PASSWORD to KeyboardType.Password,
|
||||
VaultUnlockType.PIN to KeyboardType.Number,
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
assertEquals(
|
||||
expected,
|
||||
type.unlockScreenKeyboardType,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -227,33 +227,26 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on UnlockWithPinToggle Disabled should set pin unlock to false, clear the PIN in settings, and emit ShowToast`() =
|
||||
runTest {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
isUnlockWithPinEnabled = true,
|
||||
)
|
||||
val settingsRepository: SettingsRepository = mockk() {
|
||||
every { clearUnlockPin() } just runs
|
||||
}
|
||||
val viewModel = createViewModel(
|
||||
initialState = initialState,
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.UnlockWithPinToggle.Disabled,
|
||||
)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
initialState.copy(isUnlockWithPinEnabled = false),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify { settingsRepository.clearUnlockPin() }
|
||||
fun `on UnlockWithPinToggle Disabled should set pin unlock to false and clear the PIN in settings`() {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
isUnlockWithPinEnabled = true,
|
||||
)
|
||||
val settingsRepository: SettingsRepository = mockk() {
|
||||
every { clearUnlockPin() } just runs
|
||||
}
|
||||
val viewModel = createViewModel(
|
||||
initialState = initialState,
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.UnlockWithPinToggle.Disabled,
|
||||
)
|
||||
assertEquals(
|
||||
initialState.copy(isUnlockWithPinEnabled = false),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify { settingsRepository.clearUnlockPin() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
|
@ -273,41 +266,34 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on UnlockWithPinToggle Enabled should set pin unlock to true, set the PIN in settings, and emit ShowToast`() =
|
||||
runTest {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
isUnlockWithPinEnabled = false,
|
||||
)
|
||||
val settingsRepository: SettingsRepository = mockk() {
|
||||
every { storeUnlockPin(any(), any()) } just runs
|
||||
}
|
||||
val viewModel = createViewModel(
|
||||
initialState = initialState,
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.UnlockWithPinToggle.Enabled(
|
||||
pin = "1234",
|
||||
shouldRequireMasterPasswordOnRestart = true,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
initialState.copy(isUnlockWithPinEnabled = true),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify {
|
||||
settingsRepository.storeUnlockPin(
|
||||
pin = "1234",
|
||||
shouldRequireMasterPasswordOnRestart = true,
|
||||
)
|
||||
}
|
||||
fun `on UnlockWithPinToggle Enabled should set pin unlock to true and set the PIN in settings`() {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
isUnlockWithPinEnabled = false,
|
||||
)
|
||||
val settingsRepository: SettingsRepository = mockk() {
|
||||
every { storeUnlockPin(any(), any()) } just runs
|
||||
}
|
||||
val viewModel = createViewModel(
|
||||
initialState = initialState,
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.UnlockWithPinToggle.Enabled(
|
||||
pin = "1234",
|
||||
shouldRequireMasterPasswordOnRestart = true,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
initialState.copy(isUnlockWithPinEnabled = true),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify {
|
||||
settingsRepository.storeUnlockPin(
|
||||
pin = "1234",
|
||||
shouldRequireMasterPasswordOnRestart = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on LogoutClick should show confirm log out dialog`() = runTest {
|
||||
|
|
Loading…
Add table
Reference in a new issue