mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
Add login with password flow (#1254)
This commit is contained in:
parent
f2301e15b9
commit
44728bba02
14 changed files with 270 additions and 50 deletions
|
@ -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(
|
||||
|
|
|
@ -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() },
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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<UnlockType>(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()
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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<VaultUnlockState, VaultUnlockEvent, VaultUnlockAction>(
|
||||
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<AccountSummary>,
|
||||
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 {
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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<TrustedDeviceEvent>()
|
||||
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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -77,7 +77,7 @@ class RootNavScreenTest : BaseComposeTest() {
|
|||
rootNavStateFlow.value = RootNavState.VaultLocked
|
||||
composeTestRule.runOnIdle {
|
||||
fakeNavHostController.assertLastNavigation(
|
||||
route = "vault_unlock",
|
||||
route = "vault_unlock/STANDARD",
|
||||
navOptions = expectedNavOptions,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue