mirror of
https://github.com/bitwarden/android.git
synced 2026-06-01 18:26:31 -05:00
BIT-926: account security UI (#193)
This commit is contained in:
@@ -2,94 +2,227 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.AccountSecurityAction.ConfirmLogoutClick
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.AccountSecurityAction.DismissDialog
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class AccountSecurityScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `on Log out click should send LogoutClick`() {
|
||||
val viewModel: AccountSecurityViewModel = mockk {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(AccountSecurityAction.LogoutClick) } returns Unit
|
||||
}
|
||||
private var onNavigateBackCalled = false
|
||||
|
||||
private val intentHandler = mockk<IntentHandler> {
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
private val mutableEventFlow = MutableSharedFlow<AccountSecurityEvent>(
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
)
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val viewModel = mockk<AccountSecurityViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
intentHandler = intentHandler,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Log out").performClick()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Log out click should send LogoutClick`() {
|
||||
composeTestRule.onNodeWithText("Log out").performScrollTo().performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.LogoutClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on approve login requests toggle should send LoginRequestToggle`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Use this device to approve login requests made from other devices")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.LoginRequestToggle(true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on approve login requests should be toggled on or off according to state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Use this device to approve login requests made from other devices")
|
||||
.assertIsOff()
|
||||
mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = true) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Use this device to approve login requests made from other devices")
|
||||
.assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on pending login requests click should send PendingLoginRequestsClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Pending login requests")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.PendingLoginRequestsClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on unlock with biometrics toggle should send UnlockWithBiometricToggle`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Unlock with Biometrics")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on unlock with biometrics should be toggled on or off according to state`() {
|
||||
composeTestRule.onNodeWithText("Unlock with Biometrics").assertIsOff()
|
||||
mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = true) }
|
||||
composeTestRule.onNodeWithText("Unlock with Biometrics").assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on unlock with pin toggle should send UnlockWithPinToggle`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Unlock with PIN code")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle(true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on unlock with pin code should be toggled on or off according to state`() {
|
||||
composeTestRule.onNodeWithText("Unlock with PIN code").assertIsOff()
|
||||
mutableStateFlow.update { it.copy(isUnlockWithPinEnabled = true) }
|
||||
composeTestRule.onNodeWithText("Unlock with PIN code").assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on session timeout click should send SessionTimeoutClick`() {
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.SessionTimeoutClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `session timeout should be updated on or off according to state`() {
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.assertTextEquals("Session timeout", "15 Minutes")
|
||||
mutableStateFlow.update { it.copy(sessionTimeout = "30 Minutes".asText()) }
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.assertTextEquals("Session timeout", "30 Minutes")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on session timeout action click should send SessionTimeoutActionClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Session timeout action")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.SessionTimeoutActionClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `session timeout action should be updated on or off according to state`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Session timeout action")
|
||||
.performScrollTo()
|
||||
.assertTextEquals("Session timeout action", "Lock")
|
||||
mutableStateFlow.update { it.copy(sessionTimeoutAction = SessionTimeoutAction.LOG_OUT) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Session timeout action")
|
||||
.performScrollTo()
|
||||
.assertTextEquals("Session timeout action", "Log out")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `session timeout action dialog should be displayed to state`() {
|
||||
composeTestRule.onNodeWithText("Vault timeout action").assertDoesNotExist()
|
||||
mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.SessionTimeoutAction) }
|
||||
composeTestRule.onNodeWithText("Vault timeout action").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on two-step login click should send TwoStepLoginClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Two-step login")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.TwoStepLoginClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on change master password click should send ChangeMasterPasswordClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Change master password")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.ChangeMasterPasswordClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Lock now click should send LockNowClick`() {
|
||||
composeTestRule.onNodeWithText("Lock now").performScrollTo().performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.LockNowClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on delete account click should send DeleteAccountClick`() {
|
||||
composeTestRule.onNodeWithText("Delete account").performScrollTo().performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.DeleteAccountClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on back click should send BackClick`() {
|
||||
val viewModel: AccountSecurityViewModel = mockk {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(AccountSecurityAction.BackClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithContentDescription("Back").performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.BackClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateAccountSecurity should call onNavigateToAccountSecurity`() {
|
||||
var haveCalledNavigateBack = false
|
||||
val viewModel = mockk<AccountSecurityViewModel> {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns flowOf(AccountSecurityEvent.NavigateBack)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { haveCalledNavigateBack = true },
|
||||
)
|
||||
}
|
||||
assertTrue(haveCalledNavigateBack)
|
||||
fun `on NavigateBack should call onNavigateBack`() {
|
||||
mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateBack)
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirm dialog be shown or hidden according to the state`() {
|
||||
val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
val viewModel = mockk<AccountSecurityViewModel> {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(ConfirmLogoutClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update { it.copy(shouldShowConfirmLogoutDialog = true) }
|
||||
|
||||
mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.ConfirmLogout) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Yes")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
@@ -106,51 +239,32 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `on confirm logout click should send ConfirmLogoutClick`() {
|
||||
val viewModel = mockk<AccountSecurityViewModel> {
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
DEFAULT_STATE.copy(shouldShowConfirmLogoutDialog = true),
|
||||
)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(ConfirmLogoutClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.ConfirmLogout) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Yes")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(ConfirmLogoutClick) }
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.ConfirmLogoutClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on cancel click should send DismissDialog`() {
|
||||
val viewModel = mockk<AccountSecurityViewModel> {
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
DEFAULT_STATE.copy(shouldShowConfirmLogoutDialog = true),
|
||||
)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(DismissDialog) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.ConfirmLogout) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Cancel")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(DismissDialog) }
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.DismissDialog) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = AccountSecurityState(
|
||||
shouldShowConfirmLogoutDialog = false,
|
||||
dialog = null,
|
||||
isApproveLoginRequestsEnabled = false,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
isUnlockWithPinEnabled = false,
|
||||
sessionTimeout = "15 Minutes".asText(),
|
||||
sessionTimeoutAction = SessionTimeoutAction.LOCK,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,30 +4,38 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockk(),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on AccountFingerprintPhraseClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.AccountFingerprintPhraseClick)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Display fingerprint phrase.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on BackClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockk(),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.BackClick)
|
||||
assertEquals(AccountSecurityEvent.NavigateBack, awaitItem())
|
||||
@@ -35,63 +43,203 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on LogoutClick should show confirm log out dialog`() = runTest {
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockk(),
|
||||
)
|
||||
viewModel.trySendAction(AccountSecurityAction.LogoutClick)
|
||||
viewModel.stateFlow.test {
|
||||
fun `on ChangeMasterPasswordClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.ChangeMasterPasswordClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
shouldShowConfirmLogoutDialog = true,
|
||||
),
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on DeleteAccountClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.DeleteAccountClick)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on DismissSessionTimeoutActionDialog should update shouldShowSessionTimeoutActionDialog`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(AccountSecurityAction.DismissDialog)
|
||||
assertEquals(DEFAULT_STATE.copy(dialog = null), viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on LockNowClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.LockNowClick)
|
||||
assertEquals(AccountSecurityEvent.ShowToast("Lock the app.".asText()), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on LoginRequestToggle should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.LoginRequestToggle(true))
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Handle Login requests on this device.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
viewModel.stateFlow.test {
|
||||
assertTrue(awaitItem().isApproveLoginRequestsEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on PendingLoginRequestsClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.PendingLoginRequestsClick)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SessionTimeoutActionSelect should update session timeout action`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.SessionTimeoutActionSelect(SessionTimeoutAction.LOG_OUT),
|
||||
)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(dialog = null, sessionTimeoutAction = SessionTimeoutAction.LOG_OUT),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SessionTimeoutActionClick should update shouldShowSessionTimeoutActionDialog`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(AccountSecurityAction.SessionTimeoutActionClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(dialog = AccountSecurityDialog.SessionTimeoutAction),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SessionTimeoutClick should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.SessionTimeoutClick)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Display session timeout dialog.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on TwoStepLoginClick should emit NavigateToTwoStepLogin`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.TwoStepLoginClick)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockWithBiometricToggle should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(true))
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Handle unlock with biometrics.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(isUnlockWithBiometricsEnabled = true),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockWithPinToggle should emit ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(AccountSecurityAction.UnlockWithPinToggle(true))
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(isUnlockWithPinEnabled = true),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on LogoutClick should show confirm log out dialog`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(AccountSecurityAction.LogoutClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(dialog = AccountSecurityDialog.ConfirmLogout),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on ConfirmLogoutClick should call logout and hide confirm dialog`() = runTest {
|
||||
val authRepository: AuthRepository = mockk {
|
||||
every { logout() } returns Unit
|
||||
}
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = authRepository,
|
||||
)
|
||||
val viewModel = createViewModel(authRepository = authRepository)
|
||||
viewModel.trySendAction(AccountSecurityAction.ConfirmLogoutClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
shouldShowConfirmLogoutDialog = false,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(DEFAULT_STATE.copy(dialog = null), viewModel.stateFlow.value)
|
||||
verify { authRepository.logout() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on DismissDialog should hide dialog`() = runTest {
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockk(),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(AccountSecurityAction.DismissDialog)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
shouldShowConfirmLogoutDialog = false,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(DEFAULT_STATE.copy(dialog = null), viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
authRepository: AuthRepository = mockk(relaxed = true),
|
||||
savedStateHandle: SavedStateHandle = SavedStateHandle(),
|
||||
): AccountSecurityViewModel = AccountSecurityViewModel(
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = AccountSecurityState(
|
||||
shouldShowConfirmLogoutDialog = false,
|
||||
dialog = null,
|
||||
isApproveLoginRequestsEnabled = false,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
isUnlockWithPinEnabled = false,
|
||||
sessionTimeout = "15 Minutes".asText(),
|
||||
sessionTimeoutAction = SessionTimeoutAction.LOCK,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user