mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 11:59:57 +03:00
Add initial UI flow for TDE (#1235)
This commit is contained in:
parent
bfbb8d47a6
commit
a6a4c40693
12 changed files with 300 additions and 49 deletions
|
@ -1137,11 +1137,13 @@ class AuthRepositoryImpl(
|
|||
|
||||
// Handle the Trusted Device Encryption flow
|
||||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions?.let { options ->
|
||||
handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions(
|
||||
trustedDeviceDecryptionOptions = options,
|
||||
userStateJson = userStateJson,
|
||||
privateKey = requireNotNull(loginResponse.privateKey),
|
||||
)
|
||||
loginResponse.privateKey?.let { privateKey ->
|
||||
handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions(
|
||||
trustedDeviceDecryptionOptions = options,
|
||||
userStateJson = userStateJson,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any cached data after successfully logging in.
|
||||
|
|
|
@ -23,7 +23,6 @@ import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.masterPasswordHint
|
|||
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.navigateToMasterPasswordHint
|
||||
import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword
|
||||
import com.x8bit.bitwarden.ui.auth.feature.setpassword.setPasswordDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.trustedDeviceDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin
|
||||
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination
|
||||
|
||||
|
@ -113,7 +112,6 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
|||
masterPasswordHintDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
trustedDeviceDestination()
|
||||
twoFactorLoginDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.navigation
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.loginWithDeviceDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.navigateToLoginWithDevice
|
||||
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin
|
||||
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination
|
||||
|
||||
const val TRUSTED_DEVICE_GRAPH_ROUTE: String = "trusted_device_graph"
|
||||
|
||||
/**
|
||||
* Add trusted device destinations to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.trustedDeviceGraph(navController: NavHostController) {
|
||||
navigation(
|
||||
startDestination = TRUSTED_DEVICE_ROUTE,
|
||||
route = TRUSTED_DEVICE_GRAPH_ROUTE,
|
||||
) {
|
||||
loginWithDeviceDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToTwoFactorLogin = {
|
||||
navController.navigateToTwoFactorLogin(
|
||||
emailAddress = it,
|
||||
password = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
trustedDeviceDestination(
|
||||
onNavigateToAdminApproval = {
|
||||
navController.navigateToLoginWithDevice(
|
||||
emailAddress = it,
|
||||
loginType = LoginWithDeviceType.SSO_ADMIN_APPROVAL,
|
||||
)
|
||||
},
|
||||
onNavigateToLoginWithOtherDevice = {
|
||||
navController.navigateToLoginWithDevice(
|
||||
emailAddress = it,
|
||||
loginType = LoginWithDeviceType.SSO_OTHER_DEVICE,
|
||||
)
|
||||
},
|
||||
)
|
||||
twoFactorLoginDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the trusted device graph.
|
||||
*/
|
||||
fun NavController.navigateToTrustedDeviceGraph(
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
navigate(TRUSTED_DEVICE_GRAPH_ROUTE, navOptions)
|
||||
}
|
|
@ -1,39 +1,29 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
private const val EMAIL_ADDRESS: String = "email_address"
|
||||
private const val TRUSTED_DEVICE_PREFIX: String = "trusted_device"
|
||||
private const val TRUSTED_DEVICE_ROUTE: String = "$TRUSTED_DEVICE_PREFIX/{${EMAIL_ADDRESS}}"
|
||||
|
||||
/**
|
||||
* Class to retrieve trusted device arguments from the [SavedStateHandle].
|
||||
* The route for navigating to the [TrustedDeviceScreen].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class TrustedDeviceArgs(val emailAddress: String) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
emailAddress = checkNotNull(savedStateHandle.get<String>(EMAIL_ADDRESS)),
|
||||
)
|
||||
}
|
||||
const val TRUSTED_DEVICE_ROUTE: String = "trusted_device"
|
||||
|
||||
/**
|
||||
* Add the Trusted Device Screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.trustedDeviceDestination() {
|
||||
fun NavGraphBuilder.trustedDeviceDestination(
|
||||
onNavigateToAdminApproval: (emailAddress: String) -> Unit,
|
||||
onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = TRUSTED_DEVICE_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
TrustedDeviceScreen()
|
||||
TrustedDeviceScreen(
|
||||
onNavigateToAdminApproval = onNavigateToAdminApproval,
|
||||
onNavigateToLoginWithOtherDevice = onNavigateToLoginWithOtherDevice,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,8 +31,7 @@ fun NavGraphBuilder.trustedDeviceDestination() {
|
|||
* Navigate to the Trusted Device Screen.
|
||||
*/
|
||||
fun NavController.navigateToTrustedDevice(
|
||||
emailAddress: String,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate("$TRUSTED_DEVICE_PREFIX/$emailAddress", navOptions)
|
||||
this.navigate(TRUSTED_DEVICE_ROUTE, navOptions)
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
|||
*/
|
||||
@Composable
|
||||
fun TrustedDeviceScreen(
|
||||
onNavigateToAdminApproval: (emailAddress: String) -> Unit,
|
||||
onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit,
|
||||
viewModel: TrustedDeviceViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
@ -54,6 +56,14 @@ fun TrustedDeviceScreen(
|
|||
val context = LocalContext.current
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is TrustedDeviceEvent.NavigateToApproveWithAdmin -> {
|
||||
onNavigateToAdminApproval(event.email)
|
||||
}
|
||||
|
||||
is TrustedDeviceEvent.NavigateToApproveWithDevice -> {
|
||||
onNavigateToLoginWithOtherDevice(event.email)
|
||||
}
|
||||
|
||||
is TrustedDeviceEvent.ShowToast -> {
|
||||
Toast
|
||||
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
|
||||
|
|
|
@ -25,14 +25,19 @@ class TrustedDeviceViewModel @Inject constructor(
|
|||
) : BaseViewModel<TrustedDeviceState, TrustedDeviceEvent, TrustedDeviceAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: run {
|
||||
val account = authRepository.userStateFlow.value?.activeAccount
|
||||
val trustedDevice = account?.trustedDevice
|
||||
if (trustedDevice == null) authRepository.logout()
|
||||
TrustedDeviceState(
|
||||
emailAddress = TrustedDeviceArgs(savedStateHandle).emailAddress,
|
||||
emailAddress = account?.email.orEmpty(),
|
||||
environmentLabel = environmentRepository.environment.label,
|
||||
isRemembered = false,
|
||||
showContinueButton = false,
|
||||
showOtherDeviceButton = false,
|
||||
showRequestAdminButton = false,
|
||||
showMasterPasswordButton = false,
|
||||
isRemembered = true,
|
||||
showContinueButton = trustedDevice
|
||||
?.let { !it.hasAdminApproval && !it.hasMasterPassword }
|
||||
?: false,
|
||||
showOtherDeviceButton = trustedDevice?.hasLoginApprovingDevice ?: false,
|
||||
showRequestAdminButton = trustedDevice?.hasAdminApproval ?: false,
|
||||
showMasterPasswordButton = trustedDevice?.hasMasterPassword ?: false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -61,11 +66,13 @@ class TrustedDeviceViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleApproveWithAdminClick() {
|
||||
sendEvent(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()))
|
||||
authRepository.shouldTrustDevice = state.isRemembered
|
||||
sendEvent(TrustedDeviceEvent.NavigateToApproveWithAdmin(state.emailAddress))
|
||||
}
|
||||
|
||||
private fun handleApproveWithDeviceClick() {
|
||||
sendEvent(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()))
|
||||
authRepository.shouldTrustDevice = state.isRemembered
|
||||
sendEvent(TrustedDeviceEvent.NavigateToApproveWithDevice(state.emailAddress))
|
||||
}
|
||||
|
||||
private fun handleApproveWithPasswordClick() {
|
||||
|
@ -95,6 +102,20 @@ data class TrustedDeviceState(
|
|||
* Models events for the Trusted Device screen.
|
||||
*/
|
||||
sealed class TrustedDeviceEvent {
|
||||
/**
|
||||
* Navigates to the approve with admin screen.
|
||||
*/
|
||||
data class NavigateToApproveWithAdmin(
|
||||
val email: String,
|
||||
) : TrustedDeviceEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the approve with device screen.
|
||||
*/
|
||||
data class NavigateToApproveWithDevice(
|
||||
val email: String,
|
||||
) : TrustedDeviceEvent()
|
||||
|
||||
/**
|
||||
* Displays the [message] as a toast.
|
||||
*/
|
||||
|
|
|
@ -21,6 +21,9 @@ import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPassword
|
|||
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.resetPasswordDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.setpassword.SET_PASSWORD_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword
|
||||
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.TRUSTED_DEVICE_GRAPH_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.navigateToTrustedDeviceGraph
|
||||
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.trustedDeviceGraph
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination
|
||||
|
@ -83,6 +86,7 @@ fun RootNavScreen(
|
|||
splashDestination()
|
||||
authGraph(navController)
|
||||
resetPasswordDestination()
|
||||
trustedDeviceGraph(navController)
|
||||
vaultUnlockDestination()
|
||||
vaultUnlockedGraph(navController)
|
||||
}
|
||||
|
@ -90,8 +94,9 @@ fun RootNavScreen(
|
|||
val targetRoute = when (state) {
|
||||
RootNavState.Auth -> AUTH_GRAPH_ROUTE
|
||||
RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE
|
||||
is RootNavState.SetPassword -> SET_PASSWORD_ROUTE
|
||||
RootNavState.SetPassword -> SET_PASSWORD_ROUTE
|
||||
RootNavState.Splash -> SPLASH_ROUTE
|
||||
RootNavState.TrustedDevice -> TRUSTED_DEVICE_GRAPH_ROUTE
|
||||
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
|
||||
is RootNavState.VaultUnlocked,
|
||||
is RootNavState.VaultUnlockedForAutofillSave,
|
||||
|
@ -130,8 +135,9 @@ fun RootNavScreen(
|
|||
when (val currentState = state) {
|
||||
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
|
||||
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
|
||||
is RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
|
||||
RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
|
||||
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
||||
RootNavState.TrustedDevice -> navController.navigateToTrustedDeviceGraph(rootNavOptions)
|
||||
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
|
||||
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(
|
||||
rootNavOptions,
|
||||
|
|
|
@ -54,6 +54,7 @@ class RootNavViewModel @Inject constructor(
|
|||
authRepository.updateLastActiveTime()
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun handleUserStateUpdateReceive(
|
||||
action: RootNavAction.Internal.UserStateUpdateReceive,
|
||||
) {
|
||||
|
@ -64,6 +65,9 @@ class RootNavViewModel @Inject constructor(
|
|||
|
||||
userState?.activeAccount?.needsPasswordReset == true -> RootNavState.ResetPassword
|
||||
|
||||
userState?.activeAccount?.trustedDevice?.isDeviceTrusted == false &&
|
||||
!userState.activeAccount.isVaultUnlocked -> RootNavState.TrustedDevice
|
||||
|
||||
userState == null ||
|
||||
!userState.activeAccount.isLoggedIn ||
|
||||
userState.hasPendingAccountAddition -> RootNavState.Auth
|
||||
|
@ -131,6 +135,12 @@ sealed class RootNavState : Parcelable {
|
|||
@Parcelize
|
||||
data object Splash : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show the trusted device destination.
|
||||
*/
|
||||
@Parcelize
|
||||
data object TrustedDevice : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show vault locked nav graph.
|
||||
*/
|
||||
|
|
|
@ -14,11 +14,15 @@ import io.mockk.mockk
|
|||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class TrustedDeviceScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateToAdminApprovalEmail: String? = null
|
||||
private var onNavigateToLoginWithOtherDeviceEmail: String? = null
|
||||
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<TrustedDeviceEvent>()
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
val viewModel = mockk<TrustedDeviceViewModel>(relaxed = true) {
|
||||
|
@ -31,10 +35,26 @@ class TrustedDeviceScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
TrustedDeviceScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToAdminApproval = { onNavigateToAdminApprovalEmail = it },
|
||||
onNavigateToLoginWithOtherDevice = { onNavigateToLoginWithOtherDeviceEmail = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToApproveWithDevice event should invoke onNavigateToAdminApproval`() {
|
||||
val email = "test@bitwarden.com"
|
||||
mutableEventFlow.tryEmit(TrustedDeviceEvent.NavigateToApproveWithAdmin(email))
|
||||
assertEquals(onNavigateToAdminApprovalEmail, email)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToApproveWithDevice event should invoke onNavigateToLoginWithOtherDevice`() {
|
||||
val email = "test@bitwarden.com"
|
||||
mutableEventFlow.tryEmit(TrustedDeviceEvent.NavigateToApproveWithDevice(email))
|
||||
assertEquals(onNavigateToLoginWithOtherDeviceEmail, email)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on back click should send BackClick`() {
|
||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||
|
|
|
@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
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
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -12,17 +14,32 @@ import io.mockk.just
|
|||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class TrustedDeviceViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { logout() } just runs
|
||||
}
|
||||
private val environmentRepo: FakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
|
||||
@Test
|
||||
fun `on init should logout when trusted device is not present`() {
|
||||
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
|
||||
accounts = listOf(DEFAULT_ACCOUNT.copy(trustedDevice = null)),
|
||||
)
|
||||
createViewModel()
|
||||
|
||||
verify(exactly = 1) {
|
||||
authRepository.logout()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on BackClick should logout`() {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -40,10 +57,10 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(TrustedDeviceAction.RememberToggle(isRemembered = true))
|
||||
assertEquals(DEFAULT_STATE.copy(isRemembered = true), awaitItem())
|
||||
viewModel.trySendAction(TrustedDeviceAction.RememberToggle(isRemembered = false))
|
||||
assertEquals(DEFAULT_STATE.copy(isRemembered = false), awaitItem())
|
||||
viewModel.trySendAction(TrustedDeviceAction.RememberToggle(isRemembered = true))
|
||||
assertEquals(DEFAULT_STATE.copy(isRemembered = true), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,22 +75,30 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on ApproveWithAdminClick emits ShowToast`() = runTest {
|
||||
fun `on ApproveWithAdminClick emits NavigateToApproveWithAdmin`() = runTest {
|
||||
every { authRepository.shouldTrustDevice = true } just runs
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(TrustedDeviceAction.ApproveWithAdminClick)
|
||||
assertEquals(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()), awaitItem())
|
||||
assertEquals(TrustedDeviceEvent.NavigateToApproveWithAdmin(email = EMAIL), awaitItem())
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
authRepository.shouldTrustDevice = true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on ApproveWithDeviceClick emits ShowToast`() = runTest {
|
||||
fun `on ApproveWithDeviceClick emits NavigateToApproveWithDevice`() = runTest {
|
||||
every { authRepository.shouldTrustDevice = true } just runs
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(TrustedDeviceAction.ApproveWithDeviceClick)
|
||||
assertEquals(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()), awaitItem())
|
||||
assertEquals(TrustedDeviceEvent.NavigateToApproveWithDevice(email = EMAIL), awaitItem())
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
authRepository.shouldTrustDevice = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,12 +138,44 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
private const val USER_ID: String = "userId"
|
||||
private const val EMAIL: String = "email@bitwarden.com"
|
||||
|
||||
private val DEFAULT_STATE: TrustedDeviceState = TrustedDeviceState(
|
||||
emailAddress = "email@bitwarden.com",
|
||||
emailAddress = EMAIL,
|
||||
environmentLabel = "bitwarden.com",
|
||||
isRemembered = false,
|
||||
isRemembered = true,
|
||||
showContinueButton = false,
|
||||
showOtherDeviceButton = false,
|
||||
showRequestAdminButton = false,
|
||||
showOtherDeviceButton = true,
|
||||
showRequestAdminButton = true,
|
||||
showMasterPasswordButton = false,
|
||||
)
|
||||
|
||||
private val TRUSTED_DEVICE = UserState.TrustedDevice(
|
||||
isDeviceTrusted = false,
|
||||
hasMasterPassword = false,
|
||||
hasAdminApproval = true,
|
||||
hasLoginApprovingDevice = true,
|
||||
hasResetPasswordPermission = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_ACCOUNT = UserState.Account(
|
||||
userId = USER_ID,
|
||||
name = "Active User",
|
||||
email = EMAIL,
|
||||
environment = Environment.Us,
|
||||
avatarColorHex = "#aa00aa",
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = TRUSTED_DEVICE,
|
||||
)
|
||||
|
||||
private val DEFAULT_USER_STATE = UserState(
|
||||
activeUserId = USER_ID,
|
||||
accounts = listOf(DEFAULT_ACCOUNT),
|
||||
)
|
||||
|
|
|
@ -100,6 +100,15 @@ class RootNavScreenTest : BaseComposeTest() {
|
|||
)
|
||||
}
|
||||
|
||||
// Make sure navigating to set password works as expected:
|
||||
rootNavStateFlow.value = RootNavState.TrustedDevice
|
||||
composeTestRule.runOnIdle {
|
||||
fakeNavHostController.assertLastNavigation(
|
||||
route = "trusted_device_graph",
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure navigating to vault unlocked works as expected:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlocked(activeUserId = "userId")
|
||||
composeTestRule.runOnIdle {
|
||||
|
|
|
@ -119,6 +119,75 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when the active user has an untrusted device the nav state should be TrustedDevice`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = false,
|
||||
isVaultUnlocked = false,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = UserState.TrustedDevice(
|
||||
isDeviceTrusted = false,
|
||||
hasMasterPassword = false,
|
||||
hasAdminApproval = true,
|
||||
hasLoginApprovingDevice = true,
|
||||
hasResetPasswordPermission = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(RootNavState.TrustedDevice, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an untrusted device but an unlocked vault the nav state should be Auth`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarColorHex",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = false,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = UserState.TrustedDevice(
|
||||
isDeviceTrusted = false,
|
||||
hasMasterPassword = false,
|
||||
hasAdminApproval = true,
|
||||
hasLoginApprovingDevice = true,
|
||||
hasResetPasswordPermission = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user but there are pending account additions the nav state should be Auth`() {
|
||||
|
|
Loading…
Add table
Reference in a new issue