mirror of
https://github.com/bitwarden/android.git
synced 2025-02-17 20:40:00 +03:00
BIT-2235: Add support for exporting vault data w/o passcode (#1281)
This commit is contained in:
parent
9648f720be
commit
dae98111e6
4 changed files with 281 additions and 55 deletions
|
@ -26,10 +26,11 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.semantics
|
|
||||||
import androidx.compose.ui.semantics.testTag
|
import androidx.compose.ui.semantics.testTag
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
@ -177,6 +178,9 @@ fun ExportVaultScreen(
|
||||||
onPasswordInputChanged = remember(viewModel) {
|
onPasswordInputChanged = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(it)) }
|
{ viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(it)) }
|
||||||
},
|
},
|
||||||
|
onSendCodeClicked = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ExportVaultAction.SendCodeClick) }
|
||||||
|
},
|
||||||
onExportVaultClick = { shouldShowConfirmationDialog = true },
|
onExportVaultClick = { shouldShowConfirmationDialog = true },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
|
@ -193,6 +197,7 @@ private fun ExportVaultScreenContent(
|
||||||
onExportFormatOptionSelected: (ExportVaultFormat) -> Unit,
|
onExportFormatOptionSelected: (ExportVaultFormat) -> Unit,
|
||||||
onFilePasswordInputChanged: (String) -> Unit,
|
onFilePasswordInputChanged: (String) -> Unit,
|
||||||
onPasswordInputChanged: (String) -> Unit,
|
onPasswordInputChanged: (String) -> Unit,
|
||||||
|
onSendCodeClicked: () -> Unit,
|
||||||
onExportVaultClick: () -> Unit,
|
onExportVaultClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
@ -207,7 +212,7 @@ private fun ExportVaultScreenContent(
|
||||||
BitwardenPolicyWarningText(
|
BitwardenPolicyWarningText(
|
||||||
text = stringResource(id = R.string.disable_personal_vault_export_policy_in_effect),
|
text = stringResource(id = R.string.disable_personal_vault_export_policy_in_effect),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.semantics { testTag = "DisablePrivateVaultPolicyLabel" }
|
.testTag("DisablePrivateVaultPolicyLabel")
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
@ -228,7 +233,7 @@ private fun ExportVaultScreenContent(
|
||||||
},
|
},
|
||||||
isEnabled = !state.policyPreventsExport,
|
isEnabled = !state.policyPreventsExport,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.semantics { testTag = "FileFormatPicker" }
|
.testTag("FileFormatPicker")
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
@ -245,7 +250,7 @@ private fun ExportVaultScreenContent(
|
||||||
showPasswordChange = { showPassword = it },
|
showPasswordChange = { showPassword = it },
|
||||||
hint = stringResource(id = R.string.password_used_to_export),
|
hint = stringResource(id = R.string.password_used_to_export),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.semantics { testTag = "FilePasswordEntry" }
|
.testTag("FilePasswordEntry")
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
@ -264,44 +269,87 @@ private fun ExportVaultScreenContent(
|
||||||
showPassword = showPassword,
|
showPassword = showPassword,
|
||||||
showPasswordChange = { showPassword = it },
|
showPasswordChange = { showPassword = it },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.semantics { testTag = "ConfirmFilePasswordEntry" }
|
.testTag("ConfirmFilePasswordEntry")
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
BitwardenPasswordField(
|
if (state.showSendCodeButton) {
|
||||||
label = stringResource(id = R.string.master_password),
|
|
||||||
value = state.passwordInput,
|
|
||||||
readOnly = state.policyPreventsExport,
|
|
||||||
onValueChange = onPasswordInputChanged,
|
|
||||||
modifier = Modifier
|
|
||||||
.semantics { testTag = "MasterPasswordEntry" }
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.export_vault_master_password_description),
|
text = stringResource(id = R.string.send_verification_code_to_email),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
BitwardenFilledTonalButton(
|
||||||
|
label = stringResource(R.string.send_code),
|
||||||
|
onClick = onSendCodeClicked,
|
||||||
|
isEnabled = !state.policyPreventsExport,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("SendTOTPCodeButton")
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
BitwardenPasswordField(
|
||||||
|
label = stringResource(id = R.string.verification_code),
|
||||||
|
value = state.passwordInput,
|
||||||
|
readOnly = state.policyPreventsExport,
|
||||||
|
hint = stringResource(id = R.string.confirm_your_identity),
|
||||||
|
onValueChange = onPasswordInputChanged,
|
||||||
|
keyboardType = KeyboardType.Number,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
} else {
|
||||||
|
BitwardenPasswordField(
|
||||||
|
label = stringResource(id = R.string.master_password),
|
||||||
|
value = state.passwordInput,
|
||||||
|
readOnly = state.policyPreventsExport,
|
||||||
|
onValueChange = onPasswordInputChanged,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("MasterPasswordEntry")
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.export_vault_master_password_description),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
BitwardenFilledTonalButton(
|
BitwardenFilledTonalButton(
|
||||||
label = stringResource(id = R.string.export_vault),
|
label = stringResource(id = R.string.export_vault),
|
||||||
onClick = onExportVaultClick,
|
onClick = onExportVaultClick,
|
||||||
isEnabled = !state.policyPreventsExport,
|
isEnabled = !state.policyPreventsExport,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.semantics { testTag = "ExportVaultButton" }
|
.testTag("ExportVaultButton")
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,6 +9,7 @@ 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.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||||
|
@ -42,7 +43,7 @@ private const val KEY_STATE = "state"
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ExportVaultViewModel @Inject constructor(
|
class ExportVaultViewModel @Inject constructor(
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
private val policyManager: PolicyManager,
|
policyManager: PolicyManager,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
private val vaultRepository: VaultRepository,
|
private val vaultRepository: VaultRepository,
|
||||||
private val fileManager: FileManager,
|
private val fileManager: FileManager,
|
||||||
|
@ -61,6 +62,12 @@ class ExportVaultViewModel @Inject constructor(
|
||||||
policyPreventsExport = policyManager
|
policyPreventsExport = policyManager
|
||||||
.getActivePolicies(type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT)
|
.getActivePolicies(type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT)
|
||||||
.any(),
|
.any(),
|
||||||
|
showSendCodeButton = authRepository
|
||||||
|
.userStateFlow
|
||||||
|
.value
|
||||||
|
?.activeAccount
|
||||||
|
?.trustedDevice
|
||||||
|
?.hasMasterPassword == false,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +95,7 @@ class ExportVaultViewModel @Inject constructor(
|
||||||
is ExportVaultAction.FilePasswordInputChange -> handleFilePasswordInputChanged(action)
|
is ExportVaultAction.FilePasswordInputChange -> handleFilePasswordInputChanged(action)
|
||||||
is ExportVaultAction.ExportFormatOptionSelect -> handleExportFormatOptionSelect(action)
|
is ExportVaultAction.ExportFormatOptionSelect -> handleExportFormatOptionSelect(action)
|
||||||
is ExportVaultAction.PasswordInputChanged -> handlePasswordInputChanged(action)
|
is ExportVaultAction.PasswordInputChanged -> handlePasswordInputChanged(action)
|
||||||
|
ExportVaultAction.SendCodeClick -> handleSendCodeClick()
|
||||||
is ExportVaultAction.ExportLocationReceive -> handleExportLocationReceive(action)
|
is ExportVaultAction.ExportLocationReceive -> handleExportLocationReceive(action)
|
||||||
|
|
||||||
is ExportVaultAction.Internal.ReceiveValidatePasswordResult -> {
|
is ExportVaultAction.Internal.ReceiveValidatePasswordResult -> {
|
||||||
|
@ -105,6 +113,10 @@ class ExportVaultViewModel @Inject constructor(
|
||||||
is ExportVaultAction.Internal.SaveExportDataToUriResultReceive -> {
|
is ExportVaultAction.Internal.SaveExportDataToUriResultReceive -> {
|
||||||
handleExportDataFinishedSavingToDisk(action)
|
handleExportDataFinishedSavingToDisk(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult -> {
|
||||||
|
handleReceiveVerifyOneTimePasscodeResult(action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,11 +170,19 @@ class ExportVaultViewModel @Inject constructor(
|
||||||
// Otherwise, validate the password.
|
// Otherwise, validate the password.
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
sendAction(
|
sendAction(
|
||||||
ExportVaultAction.Internal.ReceiveValidatePasswordResult(
|
if (state.showSendCodeButton) {
|
||||||
result = authRepository.validatePassword(
|
ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult(
|
||||||
password = state.passwordInput,
|
result = authRepository.verifyOneTimePasscode(
|
||||||
),
|
oneTimePasscode = state.passwordInput,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ExportVaultAction.Internal.ReceiveValidatePasswordResult(
|
||||||
|
result = authRepository.validatePassword(
|
||||||
|
password = state.passwordInput,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,6 +268,12 @@ class ExportVaultViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSendCodeClick() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
authRepository.requestOneTimePasscode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show an alert or proceed to export the vault after validating the password.
|
* Show an alert or proceed to export the vault after validating the password.
|
||||||
*/
|
*/
|
||||||
|
@ -266,27 +292,7 @@ class ExportVaultViewModel @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mutableStateFlow.update {
|
exportVaultData()
|
||||||
it.copy(dialogState = ExportVaultState.DialogState.Loading())
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
val result = vaultRepository.exportVaultDataToString(
|
|
||||||
format = state.exportFormat.toExportFormat(
|
|
||||||
password = if (state.exportFormat == ExportVaultFormat.JSON_ENCRYPTED) {
|
|
||||||
state.filePasswordInput
|
|
||||||
} else {
|
|
||||||
state.passwordInput
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
sendAction(
|
|
||||||
ExportVaultAction.Internal.ReceiveExportVaultDataToStringResult(
|
|
||||||
result = result,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -366,6 +372,45 @@ class ExportVaultViewModel @Inject constructor(
|
||||||
sendEvent(ExportVaultEvent.ShowToast(R.string.export_vault_success.asText()))
|
sendEvent(ExportVaultEvent.ShowToast(R.string.export_vault_success.asText()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleReceiveVerifyOneTimePasscodeResult(
|
||||||
|
action: ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult,
|
||||||
|
) {
|
||||||
|
when (action.result) {
|
||||||
|
VerifyOtpResult.Verified -> exportVaultData()
|
||||||
|
|
||||||
|
is VerifyOtpResult.NotVerified -> {
|
||||||
|
updateStateWithError(R.string.generic_error_message.asText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles exporting the vault data after all validation has finished.
|
||||||
|
*/
|
||||||
|
private fun exportVaultData() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(dialogState = ExportVaultState.DialogState.Loading())
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = vaultRepository.exportVaultDataToString(
|
||||||
|
format = state.exportFormat.toExportFormat(
|
||||||
|
password = if (state.exportFormat == ExportVaultFormat.JSON_ENCRYPTED) {
|
||||||
|
state.filePasswordInput
|
||||||
|
} else {
|
||||||
|
state.passwordInput
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
sendAction(
|
||||||
|
ExportVaultAction.Internal.ReceiveExportVaultDataToStringResult(
|
||||||
|
result = result,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateStateWithError(message: Text) {
|
private fun updateStateWithError(message: Text) {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
@ -393,6 +438,7 @@ data class ExportVaultState(
|
||||||
val passwordInput: String,
|
val passwordInput: String,
|
||||||
val passwordStrengthState: PasswordStrengthState,
|
val passwordStrengthState: PasswordStrengthState,
|
||||||
val policyPreventsExport: Boolean,
|
val policyPreventsExport: Boolean,
|
||||||
|
val showSendCodeButton: Boolean,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
/**
|
/**
|
||||||
* Represents the current state of any dialogs on the screen.
|
* Represents the current state of any dialogs on the screen.
|
||||||
|
@ -484,6 +530,11 @@ sealed class ExportVaultAction {
|
||||||
*/
|
*/
|
||||||
data class PasswordInputChanged(val input: String) : ExportVaultAction()
|
data class PasswordInputChanged(val input: String) : ExportVaultAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the user pressed the button to send a code in place of entering a password.
|
||||||
|
*/
|
||||||
|
data object SendCodeClick : ExportVaultAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models actions that the [ExportVaultViewModel] might send itself.
|
* Models actions that the [ExportVaultViewModel] might send itself.
|
||||||
*/
|
*/
|
||||||
|
@ -516,5 +567,12 @@ sealed class ExportVaultAction {
|
||||||
data class ReceiveValidatePasswordResult(
|
data class ReceiveValidatePasswordResult(
|
||||||
val result: ValidatePasswordResult,
|
val result: ValidatePasswordResult,
|
||||||
) : Internal()
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a result for verifying the one-time passcode has been received.
|
||||||
|
*/
|
||||||
|
data class ReceiveVerifyOneTimePasscodeResult(
|
||||||
|
val result: VerifyOtpResult,
|
||||||
|
) : Internal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,12 +247,55 @@ class ExportVaultScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `send code click should send SendCodeClick action`() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(showSendCodeButton = true)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText("Send code").performClick()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(ExportVaultAction.SendCodeClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `verification code input change should send PasswordInputChange action`() {
|
||||||
|
val input = "123"
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(showSendCodeButton = true)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText("Verification code").performTextInput(input)
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(input))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `send code button and verification input field should appear based on state`() {
|
||||||
|
composeTestRule.onNodeWithText("Send code").assertIsNotDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("Verification code").assertIsNotDisplayed()
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(showSendCodeButton = true)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText("Send code").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("Verification code").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `master password field should appear based on state`() {
|
||||||
|
composeTestRule.onNodeWithText("Master password").assertIsDisplayed()
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(showSendCodeButton = true)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithText("Master password").assertIsNotDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `password input change should send PasswordInputChange action`() {
|
fun `password input change should send PasswordInputChange action`() {
|
||||||
val input = "Test123"
|
val input = "Test123"
|
||||||
composeTestRule.onNodeWithText("Master password").performTextInput(input)
|
composeTestRule.onNodeWithText("Master password").performTextInput(input)
|
||||||
verify {
|
verify {
|
||||||
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged("Test123"))
|
viewModel.trySendAction(ExportVaultAction.PasswordInputChanged(input))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -267,4 +310,5 @@ private val DEFAULT_STATE = ExportVaultState(
|
||||||
exportData = "",
|
exportData = "",
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
passwordStrengthState = PasswordStrengthState.NONE,
|
||||||
policyPreventsExport = false,
|
policyPreventsExport = false,
|
||||||
|
showSendCodeButton = false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,8 +8,10 @@ import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
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.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
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.PolicyTypeJson
|
||||||
|
@ -105,6 +107,66 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ConfirmExportVaultClicked with verified code should call exportVaultDataToString`() {
|
||||||
|
val passcode = "1234"
|
||||||
|
val initialState = DEFAULT_STATE.copy(
|
||||||
|
passwordInput = passcode,
|
||||||
|
showSendCodeButton = true,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
authRepository.verifyOneTimePasscode(
|
||||||
|
oneTimePasscode = passcode,
|
||||||
|
)
|
||||||
|
} returns VerifyOtpResult.Verified
|
||||||
|
|
||||||
|
val viewModel = createViewModel(initialState)
|
||||||
|
|
||||||
|
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(
|
||||||
|
exportData = "data",
|
||||||
|
passwordInput = "",
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
vaultRepository.exportVaultDataToString(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ConfirmExportVaultClicked with invalid code should call exportVaultDataToString`() {
|
||||||
|
val passcode = "1234"
|
||||||
|
val initialState = DEFAULT_STATE.copy(
|
||||||
|
passwordInput = passcode,
|
||||||
|
showSendCodeButton = true,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
authRepository.verifyOneTimePasscode(
|
||||||
|
oneTimePasscode = passcode,
|
||||||
|
)
|
||||||
|
} returns VerifyOtpResult.NotVerified("Wrong")
|
||||||
|
|
||||||
|
val viewModel = createViewModel(initialState)
|
||||||
|
|
||||||
|
viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(
|
||||||
|
dialogState = ExportVaultState.DialogState.Error(
|
||||||
|
title = R.string.an_error_has_occurred.asText(),
|
||||||
|
message = R.string.generic_error_message.asText(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
vaultRepository.exportVaultDataToString(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `ConfirmExportVaultClicked should show success with valid input when export type is JSON_ENCRYPTED`() {
|
fun `ConfirmExportVaultClicked should show success with valid input when export type is JSON_ENCRYPTED`() {
|
||||||
|
@ -415,6 +477,19 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SendCodeClick should call requestOneTimePasscode`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
coEvery { authRepository.requestOneTimePasscode() } returns RequestOtpResult.Success
|
||||||
|
viewModel.trySendAction(ExportVaultAction.SendCodeClick)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
coVerify { authRepository.requestOneTimePasscode() }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ReceiveExportVaultDataToStringResult should update state to error if result is error`() {
|
fun `ReceiveExportVaultDataToStringResult should update state to error if result is error`() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -659,4 +734,5 @@ private val DEFAULT_STATE = ExportVaultState(
|
||||||
exportData = null,
|
exportData = null,
|
||||||
passwordStrengthState = PasswordStrengthState.NONE,
|
passwordStrengthState = PasswordStrengthState.NONE,
|
||||||
policyPreventsExport = false,
|
policyPreventsExport = false,
|
||||||
|
showSendCodeButton = false,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue