mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
BIT-363, BIT-1323: Add time interval options to session timeout menu (#529)
This commit is contained in:
parent
7cfdddaa81
commit
e69c4eb29e
8 changed files with 428 additions and 52 deletions
|
@ -3,6 +3,8 @@ package com.x8bit.bitwarden.data.platform.datasource.disk.di
|
|||
import android.content.SharedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSourceImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -27,4 +29,13 @@ object PlatformDiskModule {
|
|||
sharedPreferences = sharedPreferences,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSettingsDiskSource(
|
||||
sharedPreferences: SharedPreferences,
|
||||
): SettingsDiskSource =
|
||||
SettingsDiskSourceImpl(
|
||||
sharedPreferences = sharedPreferences,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,9 +2,12 @@ package com.x8bit.bitwarden.data.platform.repository.di
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -30,4 +33,17 @@ object PlatformRepositoryModule {
|
|||
authDiskSource = authDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSettingsRepository(
|
||||
authDiskSource: AuthDiskSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): SettingsRepository =
|
||||
SettingsRepositoryImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ import androidx.compose.material3.rememberTopAppBarState
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
@ -28,6 +30,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
@ -40,9 +43,11 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow
|
|||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
||||
import com.x8bit.bitwarden.ui.platform.util.displayLabel
|
||||
|
||||
/**
|
||||
* Displays the account security screen.
|
||||
|
@ -198,19 +203,13 @@ fun AccountSecurityScreen(
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = R.string.session_timeout),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AccountSecurityAction.SessionTimeoutClick) }
|
||||
SessionTimeoutRow(
|
||||
selectedVaultTimeoutType = state.vaultTimeoutType,
|
||||
onVaultTimeoutTypeSelect = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AccountSecurityAction.VaultTimeoutTypeSelect(it)) }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = state.sessionTimeout(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
)
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = R.string.session_timeout_action),
|
||||
onClick = remember(viewModel) {
|
||||
|
@ -286,6 +285,70 @@ fun AccountSecurityScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SessionTimeoutRow(
|
||||
selectedVaultTimeoutType: VaultTimeout.Type,
|
||||
onVaultTimeoutTypeSelect: (VaultTimeout.Type) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var shouldShowSelectionDialog by remember { mutableStateOf(false) }
|
||||
var shouldShowNeverTimeoutConfirmationDialog by remember { mutableStateOf(false) }
|
||||
BitwardenTextRow(
|
||||
text = stringResource(id = R.string.session_timeout),
|
||||
onClick = { shouldShowSelectionDialog = true },
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = selectedVaultTimeoutType.displayLabel(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
when {
|
||||
shouldShowSelectionDialog -> {
|
||||
val vaultTimeoutOptions = VaultTimeout.Type.entries
|
||||
BitwardenSelectionDialog(
|
||||
title = stringResource(id = R.string.session_timeout),
|
||||
onDismissRequest = { shouldShowSelectionDialog = false },
|
||||
) {
|
||||
vaultTimeoutOptions.forEach { vaultTimeoutOption ->
|
||||
BitwardenSelectionRow(
|
||||
text = vaultTimeoutOption.displayLabel,
|
||||
onClick = {
|
||||
shouldShowSelectionDialog = false
|
||||
val selectedType =
|
||||
vaultTimeoutOptions.first { it == vaultTimeoutOption }
|
||||
if (selectedType == VaultTimeout.Type.NEVER) {
|
||||
shouldShowNeverTimeoutConfirmationDialog = true
|
||||
} else {
|
||||
onVaultTimeoutTypeSelect(selectedType)
|
||||
}
|
||||
},
|
||||
isSelected = selectedVaultTimeoutType == vaultTimeoutOption,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shouldShowNeverTimeoutConfirmationDialog -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.warning),
|
||||
message = stringResource(id = R.string.never_lock_warning),
|
||||
confirmButtonText = stringResource(id = R.string.ok),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
onConfirmClick = {
|
||||
shouldShowNeverTimeoutConfirmationDialog = false
|
||||
onVaultTimeoutTypeSelect(VaultTimeout.Type.NEVER)
|
||||
},
|
||||
onDismissClick = { shouldShowNeverTimeoutConfirmationDialog = false },
|
||||
onDismissRequest = { shouldShowNeverTimeoutConfirmationDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FingerPrintPhraseDialog(
|
||||
fingerprintPhrase: Text,
|
||||
|
@ -349,7 +412,10 @@ private fun SessionTimeoutActionDialog(
|
|||
BitwardenSelectionRow(
|
||||
text = option.text,
|
||||
isSelected = option == selectedSessionTimeoutAction,
|
||||
onClick = { onActionSelect(SessionTimeoutAction.values().first { it == option }) },
|
||||
onClick = {
|
||||
onActionSelect(
|
||||
SessionTimeoutAction.values().first { it == option })
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
@ -26,6 +28,7 @@ private const val KEY_STATE = "state"
|
|||
class AccountSecurityViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<AccountSecurityState, AccountSecurityEvent, AccountSecurityAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
|
@ -35,7 +38,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
isApproveLoginRequestsEnabled = false,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
isUnlockWithPinEnabled = false,
|
||||
sessionTimeout = "15 Minutes".asText(),
|
||||
vaultTimeoutType = settingsRepository.vaultTimeout.type,
|
||||
sessionTimeoutAction = SessionTimeoutAction.LOCK,
|
||||
),
|
||||
) {
|
||||
|
@ -58,12 +61,12 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
is AccountSecurityAction.LoginRequestToggle -> handleLoginRequestToggle(action)
|
||||
AccountSecurityAction.LogoutClick -> handleLogoutClick()
|
||||
AccountSecurityAction.PendingLoginRequestsClick -> handlePendingLoginRequestsClick()
|
||||
is AccountSecurityAction.VaultTimeoutTypeSelect -> handleVaultTimeoutTypeSelect(action)
|
||||
is AccountSecurityAction.SessionTimeoutActionSelect -> {
|
||||
handleSessionTimeoutActionSelect(action)
|
||||
}
|
||||
|
||||
AccountSecurityAction.SessionTimeoutActionClick -> handleSessionTimeoutActionClick()
|
||||
AccountSecurityAction.SessionTimeoutClick -> handleSessionTimeoutClick()
|
||||
AccountSecurityAction.TwoStepLoginClick -> handleTwoStepLoginClick()
|
||||
is AccountSecurityAction.UnlockWithBiometricToggle -> {
|
||||
handleUnlockWithBiometricToggled(action)
|
||||
|
@ -119,6 +122,30 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
|
||||
}
|
||||
|
||||
private fun handleVaultTimeoutTypeSelect(action: AccountSecurityAction.VaultTimeoutTypeSelect) {
|
||||
val vaultTimeoutType = action.vaultTimeoutType
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
vaultTimeoutType = action.vaultTimeoutType,
|
||||
)
|
||||
}
|
||||
val vaultTimeout = when (vaultTimeoutType) {
|
||||
VaultTimeout.Type.IMMEDIATELY -> VaultTimeout.Immediately
|
||||
VaultTimeout.Type.ONE_MINUTE -> VaultTimeout.OneMinute
|
||||
VaultTimeout.Type.FIVE_MINUTES -> VaultTimeout.FiveMinutes
|
||||
VaultTimeout.Type.THIRTY_MINUTES -> VaultTimeout.ThirtyMinutes
|
||||
VaultTimeout.Type.ONE_HOUR -> VaultTimeout.OneHour
|
||||
VaultTimeout.Type.FOUR_HOURS -> VaultTimeout.FourHours
|
||||
VaultTimeout.Type.ON_APP_RESTART -> VaultTimeout.OnAppRestart
|
||||
VaultTimeout.Type.NEVER -> VaultTimeout.Never
|
||||
VaultTimeout.Type.CUSTOM -> VaultTimeout.Custom(vaultTimeoutInMinutes = 0)
|
||||
}
|
||||
settingsRepository.vaultTimeout = vaultTimeout
|
||||
|
||||
// TODO: Finish implementing vault timeouts (BIT-1120)
|
||||
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
|
||||
}
|
||||
|
||||
private fun handleSessionTimeoutActionSelect(
|
||||
action: AccountSecurityAction.SessionTimeoutActionSelect,
|
||||
) {
|
||||
|
@ -136,11 +163,6 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.SessionTimeoutAction) }
|
||||
}
|
||||
|
||||
private fun handleSessionTimeoutClick() {
|
||||
// TODO BIT-462: Implement session timeout
|
||||
sendEvent(AccountSecurityEvent.ShowToast("Display session timeout dialog.".asText()))
|
||||
}
|
||||
|
||||
private fun handleTwoStepLoginClick() {
|
||||
// TODO BIT-468: Implement two-step login
|
||||
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
|
||||
|
@ -171,7 +193,7 @@ data class AccountSecurityState(
|
|||
val isApproveLoginRequestsEnabled: Boolean,
|
||||
val isUnlockWithBiometricsEnabled: Boolean,
|
||||
val isUnlockWithPinEnabled: Boolean,
|
||||
val sessionTimeout: Text,
|
||||
val vaultTimeoutType: VaultTimeout.Type,
|
||||
val sessionTimeoutAction: SessionTimeoutAction,
|
||||
) : Parcelable
|
||||
|
||||
|
@ -295,6 +317,13 @@ sealed class AccountSecurityAction {
|
|||
*/
|
||||
data object PendingLoginRequestsClick : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User selected a [vaultTimeoutType].
|
||||
*/
|
||||
data class VaultTimeoutTypeSelect(
|
||||
val vaultTimeoutType: VaultTimeout.Type,
|
||||
) : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User selected a [SessionTimeoutAction].
|
||||
*/
|
||||
|
@ -307,11 +336,6 @@ sealed class AccountSecurityAction {
|
|||
*/
|
||||
data object SessionTimeoutActionClick : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User clicked session timeout.
|
||||
*/
|
||||
data object SessionTimeoutClick : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User clicked two-step login.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package com.x8bit.bitwarden.ui.platform.util
|
||||
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
||||
/**
|
||||
* Provides a human-readable display label for the given [VaultTimeout.Type].
|
||||
*/
|
||||
val VaultTimeout.Type.displayLabel: Text
|
||||
get() = when (this) {
|
||||
VaultTimeout.Type.IMMEDIATELY -> R.string.immediately
|
||||
VaultTimeout.Type.ONE_MINUTE -> R.string.one_minute
|
||||
VaultTimeout.Type.FIVE_MINUTES -> R.string.five_minutes
|
||||
VaultTimeout.Type.THIRTY_MINUTES -> R.string.thirty_minutes
|
||||
VaultTimeout.Type.ONE_HOUR -> R.string.one_hour
|
||||
VaultTimeout.Type.FOUR_HOURS -> R.string.four_hours
|
||||
VaultTimeout.Type.ON_APP_RESTART -> R.string.on_restart
|
||||
VaultTimeout.Type.NEVER -> R.string.never
|
||||
VaultTimeout.Type.CUSTOM -> R.string.custom
|
||||
}
|
||||
.asText()
|
|
@ -15,10 +15,12 @@ import androidx.compose.ui.test.onNodeWithText
|
|||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
|
@ -124,29 +126,217 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
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()) }
|
||||
.assertTextEquals("Session timeout", "30 minutes")
|
||||
mutableStateFlow.update { it.copy(vaultTimeoutType = VaultTimeout.Type.FOUR_HOURS) }
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.assertTextEquals("Session timeout", "30 Minutes")
|
||||
.assertTextEquals("Session timeout", "4 hours")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on session timeout click should show a selection dialog`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Immediately")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("1 minute")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("5 minutes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("30 minutes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("1 hour")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("4 hours")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("On app restart")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Never")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Custom")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on session timeout selection dialog cancel click should close the dialog`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on session timeout selection non-Never timeout type click should send VaultTimeoutTypeSelect and close the dialog`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("4 hours")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.VaultTimeoutTypeSelect(VaultTimeout.Type.FOUR_HOURS),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on session timeout selection Never timeout type click should show a confirmation dialog`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Never")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Warning")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
"Setting your lock options to “Never” keeps your vault available to anyone with " +
|
||||
"access to your device. If you use this option, you should ensure that you " +
|
||||
"keep your device properly protected.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on session timeout Never confirmation dialog Cancel click should close the dialog`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Never")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Warning")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) { viewModel.trySendAction(any()) }
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on session timeout Never confirmation dialog Ok click should close the dialog and emit VaultTimeoutTypeSelect`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Session timeout")
|
||||
.filterToOne(hasClickAction())
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Never")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Warning")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.VaultTimeoutTypeSelect(VaultTimeout.Type.NEVER),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -337,7 +527,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
isApproveLoginRequestsEnabled = false,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
isUnlockWithPinEnabled = false,
|
||||
sessionTimeout = "15 Minutes".asText(),
|
||||
vaultTimeoutType = VaultTimeout.Type.THIRTY_MINUTES,
|
||||
sessionTimeoutAction = SessionTimeoutAction.LOCK,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -119,6 +121,30 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on VaultTimeoutTypeSelect should update the selection and emit ShowToast()`() = runTest {
|
||||
val settingsRepository = mockk<SettingsRepository>() {
|
||||
every { vaultTimeout = any() } just runs
|
||||
}
|
||||
val viewModel = createViewModel(settingsRepository = settingsRepository)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.VaultTimeoutTypeSelect(VaultTimeout.Type.FOUR_HOURS),
|
||||
)
|
||||
assertEquals(
|
||||
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
vaultTimeoutType = VaultTimeout.Type.FOUR_HOURS,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify { settingsRepository.vaultTimeout = VaultTimeout.FourHours }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SessionTimeoutActionSelect should update session timeout action`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -148,18 +174,6 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@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()
|
||||
|
@ -235,12 +249,14 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
private fun createViewModel(
|
||||
authRepository: AuthRepository = mockk(relaxed = true),
|
||||
vaultRepository: VaultRepository = mockk(relaxed = true),
|
||||
settingsRepository: SettingsRepository = mockk(relaxed = true),
|
||||
savedStateHandle: SavedStateHandle = SavedStateHandle().apply {
|
||||
set("state", DEFAULT_STATE)
|
||||
},
|
||||
): AccountSecurityViewModel = AccountSecurityViewModel(
|
||||
authRepository = authRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
||||
|
@ -251,7 +267,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
isApproveLoginRequestsEnabled = false,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
isUnlockWithPinEnabled = false,
|
||||
sessionTimeout = "15 Minutes".asText(),
|
||||
vaultTimeoutType = VaultTimeout.Type.THIRTY_MINUTES,
|
||||
sessionTimeoutAction = SessionTimeoutAction.LOCK,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.x8bit.bitwarden.ui.platform.util
|
||||
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VaultTimeoutExtensionsTest {
|
||||
@Test
|
||||
fun `displayLabel should return the correct value for each type`() {
|
||||
mapOf(
|
||||
VaultTimeout.Type.IMMEDIATELY to R.string.immediately.asText(),
|
||||
VaultTimeout.Type.ONE_MINUTE to R.string.one_minute.asText(),
|
||||
VaultTimeout.Type.FIVE_MINUTES to R.string.five_minutes.asText(),
|
||||
VaultTimeout.Type.THIRTY_MINUTES to R.string.thirty_minutes.asText(),
|
||||
VaultTimeout.Type.ONE_HOUR to R.string.one_hour.asText(),
|
||||
VaultTimeout.Type.FOUR_HOURS to R.string.four_hours.asText(),
|
||||
VaultTimeout.Type.ON_APP_RESTART to R.string.on_restart.asText(),
|
||||
VaultTimeout.Type.NEVER to R.string.never.asText(),
|
||||
VaultTimeout.Type.CUSTOM to R.string.custom.asText(),
|
||||
)
|
||||
.forEach { (type, label) ->
|
||||
assertEquals(
|
||||
label,
|
||||
type.displayLabel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue