From a6a4c40693844776572c873fd860acebd114a3fb Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 8 Apr 2024 10:04:31 -0500 Subject: [PATCH] Add initial UI flow for TDE (#1235) --- .../auth/repository/AuthRepositoryImpl.kt | 12 +-- .../ui/auth/feature/auth/AuthNavigation.kt | 2 - .../TrustedDeviceEncryptionNavigation.kt | 60 +++++++++++++++ .../trusteddevice/TrustedDeviceNavigation.kt | 33 +++----- .../trusteddevice/TrustedDeviceScreen.kt | 10 +++ .../trusteddevice/TrustedDeviceViewModel.kt | 37 +++++++-- .../platform/feature/rootnav/RootNavScreen.kt | 10 ++- .../feature/rootnav/RootNavViewModel.kt | 10 +++ .../trusteddevice/TrustedDeviceScreenTest.kt | 20 +++++ .../TrustedDeviceViewModelTest.kt | 77 ++++++++++++++++--- .../feature/rootnav/RootNavScreenTest.kt | 9 +++ .../feature/rootnav/RootNavViewModelTest.kt | 69 +++++++++++++++++ 12 files changed, 300 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 39fa8612e5..d39e027de0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -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. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt index 6c383497e5..060d550d82 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt @@ -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() }, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt new file mode 100644 index 0000000000..a45093c508 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt @@ -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) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceNavigation.kt index e56b6ef3eb..9d4fed7bd6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceNavigation.kt @@ -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(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) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt index 00922ac5d6..43f78f4076 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt @@ -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) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModel.kt index cf9f0a31db..46d12e9930 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModel.kt @@ -25,14 +25,19 @@ class TrustedDeviceViewModel @Inject constructor( ) : BaseViewModel( 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. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index 08c58c8da8..a3224bd3b9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index 97bbb606a2..da9d038a96 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -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. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreenTest.kt index da2c9bae80..27752971a7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreenTest.kt @@ -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() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) val viewModel = mockk(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() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt index d26f13494d..85cf427637 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt @@ -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(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), +) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt index e68f825b75..54872cd6de 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt @@ -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 { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index 0e7be6525d..ecd61fcbfb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -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`() {