BIT-1321, BIT-1014: Implement Verify PIN screen (#635)

This commit is contained in:
Brian Yencho authored and Álison Fernandes committed 2024-06-20 17:08:07 +01:00
1 parent ca517c88c4
commit 880bdc8826
16 files changed
+612 -106

No files matched your search

@@ -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 {