From 44728bba025d6751b4d5be4033d0fd1b166747b2 Mon Sep 17 00:00:00 2001 From: David Perez Date: Thu, 11 Apr 2024 16:46:40 -0500 Subject: [PATCH] Add login with password flow (#1254) --- .../auth/repository/AuthRepositoryImpl.kt | 4 +- .../TrustedDeviceEncryptionNavigation.kt | 6 ++ .../trusteddevice/TrustedDeviceNavigation.kt | 2 + .../trusteddevice/TrustedDeviceScreen.kt | 5 ++ .../trusteddevice/TrustedDeviceViewModel.kt | 10 ++- .../vaultunlock/VaultUnlockNavigation.kt | 58 ++++++++++++- .../feature/vaultunlock/VaultUnlockScreen.kt | 86 ++++++++++--------- .../vaultunlock/VaultUnlockViewModel.kt | 18 +++- .../feature/vaultunlock/model/UnlockType.kt | 11 +++ .../trusteddevice/TrustedDeviceScreenTest.kt | 9 ++ .../TrustedDeviceViewModelTest.kt | 8 +- .../vaultunlock/VaultUnlockScreenTest.kt | 28 ++++++ .../vaultunlock/VaultUnlockViewModelTest.kt | 73 +++++++++++++++- .../feature/rootnav/RootNavScreenTest.kt | 2 +- 14 files changed, 270 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/model/UnlockType.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 072fdd707..f50ca9a40 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 @@ -373,13 +373,15 @@ class AuthRepositoryImpl( userId = userId, privateKey = keys.privateKey, ) + // Order matters here, we need to make sure that the vault is unlocked + // before we trust the device, to avoid state-base navigation issues. + vaultRepository.syncVaultState(userId = userId) keys.deviceKey?.let { trustDeviceResponse -> trustedDeviceManager.trustThisDevice( userId = userId, trustDeviceResponse = trustDeviceResponse, ) } - vaultRepository.syncVaultState(userId = userId) } } .fold( 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 index a45093c50..2a77ebd98 100644 --- 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 @@ -10,6 +10,8 @@ import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDevice 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 +import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToTdeVaultUnlock +import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.tdeVaultUnlockDestination const val TRUSTED_DEVICE_GRAPH_ROUTE: String = "trusted_device_graph" @@ -43,7 +45,11 @@ fun NavGraphBuilder.trustedDeviceGraph(navController: NavHostController) { loginType = LoginWithDeviceType.SSO_OTHER_DEVICE, ) }, + onNavigateToLock = { + navController.navigateToTdeVaultUnlock() + }, ) + tdeVaultUnlockDestination() twoFactorLoginDestination( onNavigateBack = { navController.popBackStack() }, ) 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 9d4fed7bd..880a9a7fc 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 @@ -16,6 +16,7 @@ const val TRUSTED_DEVICE_ROUTE: String = "trusted_device" fun NavGraphBuilder.trustedDeviceDestination( onNavigateToAdminApproval: (emailAddress: String) -> Unit, onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit, + onNavigateToLock: (emailAddress: String) -> Unit, ) { composableWithSlideTransitions( route = TRUSTED_DEVICE_ROUTE, @@ -23,6 +24,7 @@ fun NavGraphBuilder.trustedDeviceDestination( TrustedDeviceScreen( onNavigateToAdminApproval = onNavigateToAdminApproval, onNavigateToLoginWithOtherDevice = onNavigateToLoginWithOtherDevice, + onNavigateToLock = onNavigateToLock, ) } } 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 4ef533147..cec2558bb 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 @@ -52,6 +52,7 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch fun TrustedDeviceScreen( onNavigateToAdminApproval: (emailAddress: String) -> Unit, onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit, + onNavigateToLock: (emailAddress: String) -> Unit, viewModel: TrustedDeviceViewModel = hiltViewModel(), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -68,6 +69,10 @@ fun TrustedDeviceScreen( onNavigateToLoginWithOtherDevice(event.email) } + is TrustedDeviceEvent.NavigateToLockScreen -> { + onNavigateToLock(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 231f710a2..a05b850fd 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 @@ -129,7 +129,8 @@ class TrustedDeviceViewModel @Inject constructor( } private fun handleApproveWithPasswordClick() { - sendEvent(TrustedDeviceEvent.ShowToast("Not yet implemented".asText())) + authRepository.shouldTrustDevice = state.isRemembered + sendEvent(TrustedDeviceEvent.NavigateToLockScreen(state.emailAddress)) } private fun handleNotYouClick() { @@ -192,6 +193,13 @@ sealed class TrustedDeviceEvent { val email: String, ) : TrustedDeviceEvent() + /** + * Navigates to the lock screen. + */ + data class NavigateToLockScreen( + val email: String, + ) : TrustedDeviceEvent() + /** * Displays the [message] as a toast. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockNavigation.kt index 872cd2377..c14dc7761 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockNavigation.kt @@ -1,11 +1,33 @@ package com.x8bit.bitwarden.ui.auth.feature.vaultunlock +import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions +import androidx.navigation.NavType import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage +import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType -const val VAULT_UNLOCK_ROUTE: String = "vault_unlock" +private const val VAULT_UNLOCK_TYPE: String = "unlock_type" +private const val TDE_VAULT_UNLOCK_ROUTE_PREFIX: String = "tde_vault_unlock" +private const val TDE_VAULT_UNLOCK_ROUTE: String = + "$TDE_VAULT_UNLOCK_ROUTE_PREFIX/{$VAULT_UNLOCK_TYPE}" +private const val VAULT_UNLOCK_ROUTE_PREFIX: String = "vault_unlock" +const val VAULT_UNLOCK_ROUTE: String = "$VAULT_UNLOCK_ROUTE_PREFIX/{$VAULT_UNLOCK_TYPE}" + +/** + * Class to retrieve vault unlock arguments from the [SavedStateHandle]. + */ +@OmitFromCoverage +data class VaultUnlockArgs( + val unlockType: UnlockType, +) { + constructor(savedStateHandle: SavedStateHandle) : this( + unlockType = checkNotNull(savedStateHandle.get(VAULT_UNLOCK_TYPE)), + ) +} /** * Navigate to the Vault Unlock screen. @@ -13,7 +35,10 @@ const val VAULT_UNLOCK_ROUTE: String = "vault_unlock" fun NavController.navigateToVaultUnlock( navOptions: NavOptions? = null, ) { - navigate(VAULT_UNLOCK_ROUTE, navOptions) + navigate( + route = "$VAULT_UNLOCK_ROUTE_PREFIX/${UnlockType.STANDARD}", + navOptions = navOptions, + ) } /** @@ -22,6 +47,35 @@ fun NavController.navigateToVaultUnlock( fun NavGraphBuilder.vaultUnlockDestination() { composable( route = VAULT_UNLOCK_ROUTE, + arguments = listOf( + navArgument(VAULT_UNLOCK_TYPE) { type = NavType.EnumType(UnlockType::class.java) }, + ), + ) { + VaultUnlockScreen() + } +} + +/** + * Navigate to the Vault Unlock screen for TDE. + */ +fun NavController.navigateToTdeVaultUnlock( + navOptions: NavOptions? = null, +) { + navigate( + route = "$TDE_VAULT_UNLOCK_ROUTE_PREFIX/${UnlockType.TDE}", + navOptions = navOptions, + ) +} + +/** + * Add the Vault Unlock screen to the TDE nav graph. + */ +fun NavGraphBuilder.tdeVaultUnlockDestination() { + composable( + route = TDE_VAULT_UNLOCK_ROUTE, + arguments = listOf( + navArgument(VAULT_UNLOCK_TYPE) { type = NavType.EnumType(UnlockType::class.java) }, + ), ) { VaultUnlockScreen() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt index 7fcc372a1..de0853a5b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt @@ -138,11 +138,13 @@ fun VaultUnlockScreen( scrollBehavior = scrollBehavior, navigationIcon = null, actions = { - BitwardenAccountActionItem( - initials = state.initials, - color = state.avatarColor, - onClick = { accountMenuVisible = !accountMenuVisible }, - ) + if (state.showAccountMenu) { + BitwardenAccountActionItem( + initials = state.initials, + color = state.avatarColor, + onClick = { accountMenuVisible = !accountMenuVisible }, + ) + } BitwardenOverflowActionItem( menuItemDataList = persistentListOf( OverflowMenuItemData( @@ -162,29 +164,33 @@ fun VaultUnlockScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { - BitwardenPasswordField( - label = state.vaultUnlockType.unlockScreenInputLabel(), - value = state.input, - onValueChange = remember(viewModel) { - { viewModel.trySendAction(VaultUnlockAction.InputChanged(it)) } - }, - keyboardType = state.vaultUnlockType.unlockScreenKeyboardType, - showPasswordTestTag = state.vaultUnlockType.inputFieldVisibilityToggleTestTag, - modifier = Modifier - .semantics { testTag = state.vaultUnlockType.unlockScreenInputTestTag } - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = state.vaultUnlockType.unlockScreenMessage(), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.height(8.dp)) + if (!state.hideInput) { + BitwardenPasswordField( + label = state.vaultUnlockType.unlockScreenInputLabel(), + value = state.input, + onValueChange = remember(viewModel) { + { viewModel.trySendAction(VaultUnlockAction.InputChanged(it)) } + }, + keyboardType = state.vaultUnlockType.unlockScreenKeyboardType, + showPasswordTestTag = state + .vaultUnlockType + .inputFieldVisibilityToggleTestTag, + modifier = Modifier + .semantics { testTag = state.vaultUnlockType.unlockScreenInputTestTag } + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = state.vaultUnlockType.unlockScreenMessage(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) + } Text( text = stringResource( id = R.string.logged_in_as_on, @@ -220,17 +226,19 @@ fun VaultUnlockScreen( ) Spacer(modifier = Modifier.height(12.dp)) } - BitwardenFilledButton( - label = stringResource(id = R.string.unlock), - onClick = remember(viewModel) { - { viewModel.trySendAction(VaultUnlockAction.UnlockClick) } - }, - isEnabled = state.input.isNotEmpty(), - modifier = Modifier - .semantics { testTag = "UnlockVaultButton" } - .padding(horizontal = 16.dp) - .fillMaxWidth(), - ) + if (!state.hideInput) { + BitwardenFilledButton( + label = stringResource(id = R.string.unlock), + onClick = remember(viewModel) { + { viewModel.trySendAction(VaultUnlockAction.UnlockClick) } + }, + isEnabled = state.input.isNotEmpty(), + modifier = Modifier + .semantics { testTag = "UnlockVaultButton" } + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + } Spacer(modifier = Modifier.navigationBarsPadding()) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt index 021836996..27bce5646 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt @@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult +import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenErrorMessage import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.Text @@ -45,22 +46,33 @@ class VaultUnlockViewModel @Inject constructor( ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { val userState = requireNotNull(authRepository.userStateFlow.value) + val trustedDevice = userState.activeAccount.trustedDevice val accountSummaries = userState.toAccountSummaries() val activeAccountSummary = userState.toActiveAccountSummary() val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid( userId = userState.activeUserId, ) + val vaultUnlockType = userState.activeAccount.vaultUnlockType + val hasNoMasterPassword = trustedDevice?.hasMasterPassword == false + val hideInput = hasNoMasterPassword && vaultUnlockType == VaultUnlockType.MASTER_PASSWORD + val isBiometricsEnabled = userState.activeAccount.isBiometricsEnabled + if (hasNoMasterPassword && vaultUnlockType != VaultUnlockType.PIN && !isBiometricsEnabled) { + // There is no valid way to unlock this app. + authRepository.logout() + } VaultUnlockState( accountSummaries = accountSummaries, avatarColorString = activeAccountSummary.avatarColorHex, + hideInput = hideInput, initials = activeAccountSummary.initials, email = activeAccountSummary.email, dialog = null, environmentUrl = environmentRepo.environment.label, input = "", - isBiometricEnabled = userState.activeAccount.isBiometricsEnabled, + isBiometricEnabled = isBiometricsEnabled, isBiometricsValid = isBiometricsValid, - vaultUnlockType = userState.activeAccount.vaultUnlockType, + showAccountMenu = VaultUnlockArgs(savedStateHandle).unlockType == UnlockType.STANDARD, + vaultUnlockType = vaultUnlockType, ) }, ) { @@ -272,6 +284,7 @@ class VaultUnlockViewModel @Inject constructor( data class VaultUnlockState( val accountSummaries: List, private val avatarColorString: String, + val hideInput: Boolean, val initials: String, val email: String, val environmentUrl: String, @@ -279,6 +292,7 @@ data class VaultUnlockState( val input: String, val isBiometricsValid: Boolean, val isBiometricEnabled: Boolean, + val showAccountMenu: Boolean, val vaultUnlockType: VaultUnlockType, ) : Parcelable { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/model/UnlockType.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/model/UnlockType.kt new file mode 100644 index 000000000..4f49579b3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/model/UnlockType.kt @@ -0,0 +1,11 @@ +package com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model + +import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VaultUnlockScreen + +/** + * Represents the different ways you may want to display the [VaultUnlockScreen]. + */ +enum class UnlockType { + STANDARD, + TDE, +} 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 a17f5e7ed..13ef4e99f 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 @@ -26,6 +26,7 @@ class TrustedDeviceScreenTest : BaseComposeTest() { private var onNavigateToAdminApprovalEmail: String? = null private var onNavigateToLoginWithOtherDeviceEmail: String? = null + private var onNavigateToLockEmail: String? = null private val mutableEventFlow = bufferedMutableSharedFlow() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) @@ -41,6 +42,7 @@ class TrustedDeviceScreenTest : BaseComposeTest() { viewModel = viewModel, onNavigateToAdminApproval = { onNavigateToAdminApprovalEmail = it }, onNavigateToLoginWithOtherDevice = { onNavigateToLoginWithOtherDeviceEmail = it }, + onNavigateToLock = { onNavigateToLockEmail = it }, ) } } @@ -59,6 +61,13 @@ class TrustedDeviceScreenTest : BaseComposeTest() { assertEquals(onNavigateToLoginWithOtherDeviceEmail, email) } + @Test + fun `on NavigateToLockScreen event should invoke NavigateToLockScreen`() { + val email = "test@bitwarden.com" + mutableEventFlow.tryEmit(TrustedDeviceEvent.NavigateToLockScreen(email)) + assertEquals(onNavigateToLockEmail, 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 2c60f9ae5..daf2d198d 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 @@ -31,6 +31,7 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() { private val authRepository: AuthRepository = mockk { every { authStateFlow } returns mutableAuthStateFlow every { userStateFlow } returns mutableUserStateFlow + every { shouldTrustDevice = any() } just runs every { logout() } just runs } private val environmentRepo: FakeEnvironmentRepository = FakeEnvironmentRepository() @@ -196,12 +197,15 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() { } @Test - fun `on ApproveWithPasswordClick emits ShowToast`() = runTest { + fun `on ApproveWithPasswordClick emits NavigateToLockScreen`() = runTest { val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(TrustedDeviceAction.ApproveWithPasswordClick) - assertEquals(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()), awaitItem()) + assertEquals(TrustedDeviceEvent.NavigateToLockScreen(email = EMAIL), awaitItem()) + } + verify(exactly = 1) { + authRepository.shouldTrustDevice = true } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt index 915611e2a..e3cf9a481 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt @@ -382,6 +382,32 @@ class VaultUnlockScreenTest : BaseComposeTest() { viewModel.trySendAction(VaultUnlockAction.BiometricsLockOut) } } + + @Test + fun `account button should update according to state`() { + mutableStateFlow.update { it.copy(showAccountMenu = true) } + composeTestRule.onNodeWithText("AU").assertIsDisplayed() + + mutableStateFlow.update { it.copy(showAccountMenu = false) } + composeTestRule.onNodeWithText("AU").assertDoesNotExist() + } + + @Test + fun `input field and unlock button should update according to state`() { + mutableStateFlow.update { it.copy(hideInput = false) } + composeTestRule.onNodeWithText("Master password").assertIsDisplayed() + composeTestRule + .onNodeWithText("Your vault is locked. Verify your master password to continue.") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Unlock").assertIsDisplayed() + + mutableStateFlow.update { it.copy(hideInput = true) } + composeTestRule.onNodeWithText("Master password").assertDoesNotExist() + composeTestRule + .onNodeWithText("Your vault is locked. Verify your master password to continue.") + .assertDoesNotExist() + composeTestRule.onNodeWithText("Unlock").assertDoesNotExist() + } } private const val DEFAULT_ENVIRONMENT_URL: String = "vault.bitwarden.com" @@ -419,9 +445,11 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState( dialog = null, email = "bit@bitwarden.com", environmentUrl = DEFAULT_ENVIRONMENT_URL, + hideInput = false, initials = "AU", input = "", isBiometricsValid = true, isBiometricEnabled = true, + showAccountMenu = true, vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt index 54c836260..5dc3aff36 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt @@ -14,6 +14,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentReposito import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult +import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary @@ -68,6 +69,60 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { assertEquals(state, viewModel.stateFlow.value) } + @Test + fun `on init should logout when has no master password, no pin, and no biometrics`() { + mutableUserStateFlow.value = DEFAULT_USER_STATE.copy( + accounts = listOf( + DEFAULT_ACCOUNT.copy( + vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, + isBiometricsEnabled = false, + trustedDevice = TRUSTED_DEVICE, + ), + ), + ) + createViewModel() + + verify(exactly = 1) { + authRepository.logout() + } + } + + @Test + fun `on init should not logout when has no master password and no pin, with biometrics`() { + mutableUserStateFlow.value = DEFAULT_USER_STATE.copy( + accounts = listOf( + DEFAULT_ACCOUNT.copy( + vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, + isBiometricsEnabled = true, + trustedDevice = TRUSTED_DEVICE, + ), + ), + ) + createViewModel() + + verify(exactly = 0) { + authRepository.logout() + } + } + + @Test + fun `on init should not logout when has no master password and no biometrics, with pin`() { + mutableUserStateFlow.value = DEFAULT_USER_STATE.copy( + accounts = listOf( + DEFAULT_ACCOUNT.copy( + vaultUnlockType = VaultUnlockType.PIN, + isBiometricsEnabled = false, + trustedDevice = TRUSTED_DEVICE, + ), + ), + ) + createViewModel() + + verify(exactly = 0) { + authRepository.logout() + } + } + @Test fun `environment url should update when environment repo emits an update`() { val viewModel = createViewModel() @@ -714,12 +769,16 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { } private fun createViewModel( - state: VaultUnlockState? = DEFAULT_STATE, + state: VaultUnlockState? = null, + unlockType: UnlockType = UnlockType.STANDARD, environmentRepo: EnvironmentRepository = environmentRepository, vaultRepo: VaultRepository = vaultRepository, biometricsEncryptionManager: BiometricsEncryptionManager = encryptionManager, ): VaultUnlockViewModel = VaultUnlockViewModel( - savedStateHandle = SavedStateHandle().apply { set("state", state) }, + savedStateHandle = SavedStateHandle().apply { + set("state", state) + set("unlock_type", unlockType) + }, authRepository = authRepository, vaultRepo = vaultRepo, environmentRepo = environmentRepo, @@ -742,15 +801,25 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState( ), avatarColorString = "#aa00aa", email = "active@bitwarden.com", + hideInput = false, initials = "AU", dialog = null, environmentUrl = Environment.Us.label, input = "", isBiometricsValid = true, isBiometricEnabled = false, + showAccountMenu = true, vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, ) +private val TRUSTED_DEVICE: UserState.TrustedDevice = UserState.TrustedDevice( + isDeviceTrusted = false, + hasMasterPassword = false, + hasAdminApproval = false, + hasLoginApprovingDevice = false, + hasResetPasswordPermission = false, +) + private val DEFAULT_ACCOUNT = UserState.Account( userId = "activeUserId", name = "Active User", 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 54872cd6d..6e65af36d 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 @@ -77,7 +77,7 @@ class RootNavScreenTest : BaseComposeTest() { rootNavStateFlow.value = RootNavState.VaultLocked composeTestRule.runOnIdle { fakeNavHostController.assertLastNavigation( - route = "vault_unlock", + route = "vault_unlock/STANDARD", navOptions = expectedNavOptions, ) }