mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-2109: Add file password strength indicator to vault export (#1196)
This commit is contained in:
parent
b2005f01c1
commit
07ddb71cfc
3 changed files with 202 additions and 3 deletions
|
@ -5,7 +5,9 @@ import android.os.Parcelable
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
|
@ -21,6 +23,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.exportvault.model.toExpo
|
|||
import com.x8bit.bitwarden.ui.platform.util.fileExtension
|
||||
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
@ -49,6 +52,7 @@ class ExportVaultViewModel @Inject constructor(
|
|||
?: ExportVaultState(
|
||||
confirmFilePasswordInput = "",
|
||||
dialogState = null,
|
||||
email = requireNotNull(authRepository.userStateFlow.value?.activeAccount?.email),
|
||||
exportData = null,
|
||||
exportFormat = ExportVaultFormat.JSON,
|
||||
filePasswordInput = "",
|
||||
|
@ -59,6 +63,12 @@ class ExportVaultViewModel @Inject constructor(
|
|||
.any(),
|
||||
),
|
||||
) {
|
||||
/**
|
||||
* Keeps track of async request to get password strength. Should be cancelled
|
||||
* when user input changes.
|
||||
*/
|
||||
private var passwordStrengthJob: Job = Job().apply { complete() }
|
||||
|
||||
init {
|
||||
// As state updates, write to saved state handle.
|
||||
stateFlow
|
||||
|
@ -88,6 +98,10 @@ class ExportVaultViewModel @Inject constructor(
|
|||
handleReceivePrepareVaultDataResult(action)
|
||||
}
|
||||
|
||||
is ExportVaultAction.Internal.ReceivePasswordStrengthResult -> {
|
||||
handleReceivePasswordStrengthResult(action)
|
||||
}
|
||||
|
||||
is ExportVaultAction.Internal.SaveExportDataToUriResultReceive -> {
|
||||
handleExportDataFinishedSavingToDisk(action)
|
||||
}
|
||||
|
@ -182,6 +196,21 @@ class ExportVaultViewModel @Inject constructor(
|
|||
mutableStateFlow.update {
|
||||
it.copy(filePasswordInput = action.input)
|
||||
}
|
||||
// Update password strength
|
||||
passwordStrengthJob.cancel()
|
||||
if (action.input.isEmpty()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(passwordStrengthState = PasswordStrengthState.NONE)
|
||||
}
|
||||
} else {
|
||||
passwordStrengthJob = viewModelScope.launch {
|
||||
val result = authRepository.getPasswordStrength(
|
||||
email = state.email,
|
||||
password = action.input,
|
||||
)
|
||||
trySendAction(ExportVaultAction.Internal.ReceivePasswordStrengthResult(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -266,6 +295,31 @@ class ExportVaultViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleReceivePasswordStrengthResult(
|
||||
action: ExportVaultAction.Internal.ReceivePasswordStrengthResult,
|
||||
) {
|
||||
when (val result = action.result) {
|
||||
is PasswordStrengthResult.Success -> {
|
||||
val updatedState = when (result.passwordStrength) {
|
||||
PasswordStrength.LEVEL_0 -> PasswordStrengthState.WEAK_1
|
||||
PasswordStrength.LEVEL_1 -> PasswordStrengthState.WEAK_2
|
||||
PasswordStrength.LEVEL_2 -> PasswordStrengthState.WEAK_3
|
||||
PasswordStrength.LEVEL_3 -> PasswordStrengthState.GOOD
|
||||
PasswordStrength.LEVEL_4 -> PasswordStrengthState.STRONG
|
||||
}
|
||||
mutableStateFlow.update { oldState ->
|
||||
oldState.copy(
|
||||
passwordStrengthState = updatedState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PasswordStrengthResult.Error -> {
|
||||
// Leave UI the same
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleExportDataFinishedSavingToDisk(
|
||||
action: ExportVaultAction.Internal.SaveExportDataToUriResultReceive,
|
||||
) {
|
||||
|
@ -298,6 +352,7 @@ data class ExportVaultState(
|
|||
val exportData: String? = null,
|
||||
val confirmFilePasswordInput: String,
|
||||
val dialogState: DialogState?,
|
||||
val email: String,
|
||||
val exportFormat: ExportVaultFormat,
|
||||
val filePasswordInput: String,
|
||||
val passwordInput: String,
|
||||
|
@ -413,6 +468,13 @@ sealed class ExportVaultAction {
|
|||
val result: ExportVaultDataResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the result for getting the password strength has been received.
|
||||
*/
|
||||
data class ReceivePasswordStrengthResult(
|
||||
val result: PasswordStrengthResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a validate password result has been received.
|
||||
*/
|
||||
|
|
|
@ -260,6 +260,7 @@ class ExportVaultScreenTest : BaseComposeTest() {
|
|||
private val DEFAULT_STATE = ExportVaultState(
|
||||
confirmFilePasswordInput = "",
|
||||
dialogState = null,
|
||||
email = "test@bitwarden.com",
|
||||
exportFormat = ExportVaultFormat.JSON,
|
||||
filePasswordInput = "",
|
||||
passwordInput = "",
|
||||
|
|
|
@ -4,9 +4,13 @@ import android.net.Uri
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
|
@ -20,6 +24,8 @@ import io.mockk.coEvery
|
|||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
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
|
||||
|
@ -28,7 +34,10 @@ import java.time.Instant
|
|||
import java.time.ZoneOffset
|
||||
|
||||
class ExportVaultViewModelTest : BaseViewModelTest() {
|
||||
private val authRepository: AuthRepository = mockk()
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
|
||||
private val policyManager: PolicyManager = mockk {
|
||||
every {
|
||||
|
@ -61,6 +70,7 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
|||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify { authRepository.userStateFlow }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -147,6 +157,11 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
|||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.validatePassword(
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -209,15 +224,31 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `FilePasswordInputChanged should update the file password input in the state`() {
|
||||
val password = "Test123"
|
||||
coEvery {
|
||||
authRepository.getPasswordStrength(
|
||||
email = EMAIL_ADDRESS,
|
||||
password = password,
|
||||
)
|
||||
} returns PasswordStrengthResult.Success(
|
||||
passwordStrength = PasswordStrength.LEVEL_4,
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ExportVaultAction.FilePasswordInputChange("Test123"))
|
||||
viewModel.trySendAction(ExportVaultAction.FilePasswordInputChange(password))
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
filePasswordInput = "Test123",
|
||||
filePasswordInput = password,
|
||||
passwordStrengthState = PasswordStrengthState.STRONG,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
coVerify {
|
||||
authRepository.getPasswordStrength(
|
||||
email = EMAIL_ADDRESS,
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -277,6 +308,89 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ReceivePasswordStrengthResult should update password strength state`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordStrengthState = PasswordStrengthState.NONE,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(
|
||||
ExportVaultAction.Internal.ReceivePasswordStrengthResult(
|
||||
PasswordStrengthResult.Success(
|
||||
PasswordStrength.LEVEL_0,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_1,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(
|
||||
ExportVaultAction.Internal.ReceivePasswordStrengthResult(
|
||||
PasswordStrengthResult.Success(
|
||||
PasswordStrength.LEVEL_1,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_2,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(
|
||||
ExportVaultAction.Internal.ReceivePasswordStrengthResult(
|
||||
PasswordStrengthResult.Success(
|
||||
PasswordStrength.LEVEL_2,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordStrengthState = PasswordStrengthState.WEAK_3,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(
|
||||
ExportVaultAction.Internal.ReceivePasswordStrengthResult(
|
||||
PasswordStrengthResult.Success(
|
||||
PasswordStrength.LEVEL_3,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordStrengthState = PasswordStrengthState.GOOD,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(
|
||||
ExportVaultAction.Internal.ReceivePasswordStrengthResult(
|
||||
PasswordStrengthResult.Success(
|
||||
PasswordStrength.LEVEL_4,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordStrengthState = PasswordStrengthState.STRONG,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ExportLocationReceive should update state to error if exportData is null`() {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -364,9 +478,31 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
private const val EMAIL_ADDRESS = "active@bitwarden.com"
|
||||
private val DEFAULT_USER_STATE = UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "Active User",
|
||||
email = EMAIL_ADDRESS,
|
||||
avatarColorHex = "#aa00aa",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE = ExportVaultState(
|
||||
confirmFilePasswordInput = "",
|
||||
dialogState = null,
|
||||
email = EMAIL_ADDRESS,
|
||||
exportFormat = ExportVaultFormat.JSON,
|
||||
filePasswordInput = "",
|
||||
passwordInput = "",
|
||||
|
|
Loading…
Add table
Reference in a new issue