mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 05:49:44 -05:00
BITAU-184 Allow user to save to Bitwarden when adding a code manually (#263)
This commit is contained in:
@@ -37,7 +37,6 @@ import com.bitwarden.authenticator.R
|
||||
import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.authenticator.ui.platform.base.util.toAnnotatedString
|
||||
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
@@ -201,17 +200,19 @@ fun ManualCodeEntryScreen(
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenFilledTonalButton(
|
||||
label = stringResource(id = R.string.add_code),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit) }
|
||||
SaveManualCodeButtons(
|
||||
state = state.buttonState,
|
||||
onSaveLocallyClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick)
|
||||
}
|
||||
},
|
||||
onSaveToBitwardenClick = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveToBitwardenClick)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "AddCodeButton" }
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.once_the_key_is_successfully_entered),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
|
||||
@@ -8,10 +8,15 @@ import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.Aut
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
|
||||
import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager
|
||||
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
|
||||
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
|
||||
import com.bitwarden.authenticator.data.authenticator.repository.util.isSyncWithBitwardenEnabled
|
||||
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.authenticator.ui.platform.base.util.Text
|
||||
import com.bitwarden.authenticator.ui.platform.base.util.asText
|
||||
import com.bitwarden.authenticator.ui.platform.base.util.isBase32
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
|
||||
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -26,24 +31,37 @@ private const val KEY_STATE = "state"
|
||||
*
|
||||
*/
|
||||
@HiltViewModel
|
||||
@Suppress("TooManyFunctions")
|
||||
class ManualCodeEntryViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val authenticatorRepository: AuthenticatorRepository,
|
||||
private val authenticatorBridgeManager: AuthenticatorBridgeManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
) : BaseViewModel<ManualCodeEntryState, ManualCodeEntryEvent, ManualCodeEntryAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: ManualCodeEntryState(code = "", issuer = "", dialog = null),
|
||||
?: ManualCodeEntryState(
|
||||
code = "",
|
||||
issuer = "",
|
||||
dialog = null,
|
||||
buttonState = deriveButtonState(
|
||||
sharedCodesState = authenticatorRepository.sharedCodesStateFlow.value,
|
||||
defaultSaveOption = settingsRepository.defaultSaveOption,
|
||||
),
|
||||
),
|
||||
) {
|
||||
override fun handleAction(action: ManualCodeEntryAction) {
|
||||
when (action) {
|
||||
is ManualCodeEntryAction.CloseClick -> handleCloseClick()
|
||||
is ManualCodeEntryAction.CodeTextChange -> handleCodeTextChange(action)
|
||||
is ManualCodeEntryAction.IssuerTextChange -> handleIssuerTextChange(action)
|
||||
is ManualCodeEntryAction.CodeSubmit -> handleCodeSubmit()
|
||||
is ManualCodeEntryAction.ScanQrCodeTextClick -> handleScanQrCodeTextClick()
|
||||
is ManualCodeEntryAction.SettingsClick -> handleSettingsClick()
|
||||
ManualCodeEntryAction.DismissDialog -> {
|
||||
handleDialogDismiss()
|
||||
}
|
||||
|
||||
ManualCodeEntryAction.SaveLocallyClick -> handleSaveLocallyClick()
|
||||
ManualCodeEntryAction.SaveToBitwardenClick -> handleSaveToBitwardenClick()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +85,11 @@ class ManualCodeEntryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCodeSubmit() {
|
||||
private fun handleSaveLocallyClick() = handleCodeSubmit(saveToBitwarden = false)
|
||||
|
||||
private fun handleSaveToBitwardenClick() = handleCodeSubmit(saveToBitwarden = true)
|
||||
|
||||
private fun handleCodeSubmit(saveToBitwarden: Boolean) {
|
||||
val isSteamCode = state.code.startsWith(TotpCodeManager.STEAM_CODE_PREFIX)
|
||||
val sanitizedCode = state.code
|
||||
.replace(" ", "")
|
||||
@@ -87,6 +109,38 @@ class ManualCodeEntryViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
if (saveToBitwarden) {
|
||||
// Save to Bitwarden by kicking off save to Bitwarden flow:
|
||||
saveValidCodeToBitwarden(sanitizedCode)
|
||||
} else {
|
||||
// Save locally by giving entity to AuthRepository and navigating back:
|
||||
saveValidCodeLocally(sanitizedCode, isSteamCode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveValidCodeToBitwarden(sanitizedCode: String) {
|
||||
val didLaunchSaveToBitwarden = authenticatorBridgeManager
|
||||
.startAddTotpLoginItemFlow(
|
||||
totpUri = "otpauth://totp/?secret=$sanitizedCode&issuer=${state.issuer}",
|
||||
)
|
||||
if (!didLaunchSaveToBitwarden) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = ManualCodeEntryState.DialogState.Error(
|
||||
title = R.string.something_went_wrong.asText(),
|
||||
message = R.string.please_try_again.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
sendEvent(ManualCodeEntryEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveValidCodeLocally(
|
||||
sanitizedCode: String,
|
||||
isSteamCode: Boolean,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
authenticatorRepository.createItem(
|
||||
AuthenticatorItemEntity(
|
||||
@@ -133,6 +187,22 @@ class ManualCodeEntryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun deriveButtonState(
|
||||
sharedCodesState: SharedVerificationCodesState,
|
||||
defaultSaveOption: DefaultSaveOption,
|
||||
): ManualCodeEntryState.ButtonState {
|
||||
// If syncing with Bitwarden is not enabled, show local save only:
|
||||
if (!sharedCodesState.isSyncWithBitwardenEnabled) {
|
||||
return ManualCodeEntryState.ButtonState.LocalOnly
|
||||
}
|
||||
// Otherwise, show save options based on user's preferences:
|
||||
return when (defaultSaveOption) {
|
||||
DefaultSaveOption.NONE -> ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary
|
||||
DefaultSaveOption.BITWARDEN_APP -> ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary
|
||||
DefaultSaveOption.LOCAL -> ManualCodeEntryState.ButtonState.SaveLocallyPrimary
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models state of the manual entry screen.
|
||||
*/
|
||||
@@ -141,6 +211,7 @@ data class ManualCodeEntryState(
|
||||
val code: String,
|
||||
val issuer: String,
|
||||
val dialog: DialogState?,
|
||||
val buttonState: ButtonState,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
@@ -166,6 +237,31 @@ data class ManualCodeEntryState(
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models what variation of button states should be shown.
|
||||
*/
|
||||
@Parcelize
|
||||
sealed class ButtonState : Parcelable {
|
||||
|
||||
/**
|
||||
* Show only save locally option.
|
||||
*/
|
||||
@Parcelize
|
||||
data object LocalOnly : ButtonState()
|
||||
|
||||
/**
|
||||
* Show both save locally and save to Bitwarden, with Bitwarden being the primary option.
|
||||
*/
|
||||
@Parcelize
|
||||
data object SaveToBitwardenPrimary : ButtonState()
|
||||
|
||||
/**
|
||||
* Show both save locally and save to Bitwarden, with locally being the primary option.
|
||||
*/
|
||||
@Parcelize
|
||||
data object SaveLocallyPrimary : ButtonState()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,9 +301,14 @@ sealed class ManualCodeEntryAction {
|
||||
data object CloseClick : ManualCodeEntryAction()
|
||||
|
||||
/**
|
||||
* The user has submitted a code.
|
||||
* The user clicked the save locally button.
|
||||
*/
|
||||
data object CodeSubmit : ManualCodeEntryAction()
|
||||
data object SaveLocallyClick : ManualCodeEntryAction()
|
||||
|
||||
/**
|
||||
* Th user clicked the save to Bitwarden button.
|
||||
*/
|
||||
data object SaveToBitwardenClick : ManualCodeEntryAction()
|
||||
|
||||
/**
|
||||
* The user has changed the code text.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.authenticator.R
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
|
||||
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenOutlinedButton
|
||||
|
||||
/**
|
||||
* Displays save buttons for saving a manually entered code.
|
||||
*
|
||||
* @param state State of the buttons to show.
|
||||
* @param onSaveLocallyClick Callback invoked when the user clicks save locally.
|
||||
* @param onSaveToBitwardenClick Callback invoked when the user clicks save to Bitwarden.
|
||||
*/
|
||||
@Composable
|
||||
fun SaveManualCodeButtons(
|
||||
state: ManualCodeEntryState.ButtonState,
|
||||
onSaveLocallyClick: () -> Unit,
|
||||
onSaveToBitwardenClick: () -> Unit,
|
||||
) {
|
||||
|
||||
when (state) {
|
||||
ManualCodeEntryState.ButtonState.LocalOnly -> {
|
||||
BitwardenFilledTonalButton(
|
||||
label = stringResource(id = R.string.add_code),
|
||||
onClick = onSaveLocallyClick,
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "AddCodeButton" }
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
ManualCodeEntryState.ButtonState.SaveLocallyPrimary -> {
|
||||
Column {
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.add_code_locally),
|
||||
onClick = onSaveLocallyClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(R.string.add_code_to_bitwarden),
|
||||
onClick = onSaveToBitwardenClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary -> {
|
||||
Column {
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.add_code_to_bitwarden),
|
||||
onClick = onSaveToBitwardenClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(R.string.add_code_locally),
|
||||
onClick = onSaveLocallyClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,4 +145,6 @@
|
||||
<string name="choose_save_location_message">Save this authenticator key here, or add it to a login in your Bitwarden app.</string>
|
||||
<string name="save_option_as_default">Save option as default</string>
|
||||
<string name="account_synced_from_bitwarden_app">Account synced from Bitwarden app</string>
|
||||
<string name="add_code_to_bitwarden">Add code to Bitwarden</string>
|
||||
<string name="add_code_locally">Add code locally</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.authenticator.ui.platform.base.BaseComposeTest
|
||||
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
|
||||
import com.bitwarden.authenticator.ui.platform.manager.permissions.FakePermissionManager
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ManualCodeEntryScreenTest : BaseComposeTest() {
|
||||
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<ManualCodeEntryEvent>()
|
||||
|
||||
private val viewModel: ManualCodeEntryViewModel = mockk {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { trySendAction(any()) } just runs
|
||||
}
|
||||
|
||||
private val intentManager: IntentManager = mockk()
|
||||
private val permissionsManager = FakePermissionManager()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
ManualCodeEntryScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToQrCodeScreen = {},
|
||||
viewModel = viewModel,
|
||||
intentManager = intentManager,
|
||||
permissionsManager = permissionsManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Add code click should emit SaveLocallyClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Add code")
|
||||
.performClick()
|
||||
|
||||
// Make sure save to bitwaren isn't showing:
|
||||
composeTestRule
|
||||
.onNodeWithText("Add code to Bitwarden")
|
||||
.assertDoesNotExist()
|
||||
|
||||
verify { viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Add code to Bitwarden click should emit SaveToBitwardenClick`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(buttonState = ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Add code to Bitwarden")
|
||||
.performClick()
|
||||
|
||||
// Make sure locally only save isn't showing:
|
||||
composeTestRule
|
||||
.onNodeWithText("Add code")
|
||||
.assertDoesNotExist()
|
||||
|
||||
// Make sure locally option is showing:
|
||||
composeTestRule
|
||||
.onNodeWithText("Add code locally")
|
||||
.assertIsDisplayed()
|
||||
|
||||
verify { viewModel.trySendAction(ManualCodeEntryAction.SaveToBitwardenClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Add code locally click should emit SaveLocallyClick`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(buttonState = ManualCodeEntryState.ButtonState.SaveLocallyPrimary)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Add code locally")
|
||||
.performClick()
|
||||
|
||||
// Make sure locally only save isn't showing:
|
||||
composeTestRule
|
||||
.onNodeWithText("Add code")
|
||||
.assertDoesNotExist()
|
||||
|
||||
// Make sure save to bitwarden option is showing:
|
||||
composeTestRule
|
||||
.onNodeWithText("Add code to Bitwarden")
|
||||
.assertIsDisplayed()
|
||||
|
||||
verify { viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick) }
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = ManualCodeEntryState(
|
||||
code = "",
|
||||
issuer = "",
|
||||
dialog = null,
|
||||
buttonState = ManualCodeEntryState.ButtonState.LocalOnly,
|
||||
)
|
||||
@@ -7,14 +7,20 @@ import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.Aut
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
|
||||
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
|
||||
import com.bitwarden.authenticator.data.authenticator.repository.model.CreateItemResult
|
||||
import com.bitwarden.authenticator.data.authenticator.repository.model.SharedVerificationCodesState
|
||||
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.bitwarden.authenticator.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.authenticator.ui.platform.base.util.asText
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
|
||||
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -24,7 +30,14 @@ import java.util.UUID
|
||||
|
||||
class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mockAuthenticatorRepository = mockk<AuthenticatorRepository>()
|
||||
private val mockAuthenticatorRepository = mockk<AuthenticatorRepository> {
|
||||
every { sharedCodesStateFlow } returns
|
||||
MutableStateFlow(SharedVerificationCodesState.SyncNotEnabled)
|
||||
}
|
||||
private val mockSettingRepository = mockk<SettingsRepository> {
|
||||
every { defaultSaveOption } returns DefaultSaveOption.NONE
|
||||
}
|
||||
private val mockAuthenticatorBridgeManager = mockk<AuthenticatorBridgeManager>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
@@ -43,6 +56,7 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
code = "ABCD",
|
||||
issuer = "mockIssuer",
|
||||
dialog = null,
|
||||
buttonState = ManualCodeEntryState.ButtonState.LocalOnly,
|
||||
)
|
||||
val viewModel = createViewModel(initialState = initialState)
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
@@ -54,6 +68,57 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `initial button state should be SaveToBitwardenPrimary when sync is enabled and default save option is BITWARDEN_APP`() {
|
||||
every {
|
||||
mockAuthenticatorRepository.sharedCodesStateFlow
|
||||
} returns MutableStateFlow(SharedVerificationCodesState.Success(emptyList()))
|
||||
every { mockSettingRepository.defaultSaveOption } returns DefaultSaveOption.BITWARDEN_APP
|
||||
|
||||
val viewModel = createViewModel(initialState = null)
|
||||
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
buttonState = ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary,
|
||||
)
|
||||
verify { mockSettingRepository.defaultSaveOption }
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `initial button state should be SaveLocallyPrimary when sync is enabled and default save option is LOCAL`() {
|
||||
every {
|
||||
mockAuthenticatorRepository.sharedCodesStateFlow
|
||||
} returns MutableStateFlow(SharedVerificationCodesState.Success(emptyList()))
|
||||
every { mockSettingRepository.defaultSaveOption } returns DefaultSaveOption.LOCAL
|
||||
|
||||
val viewModel = createViewModel(initialState = null)
|
||||
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
buttonState = ManualCodeEntryState.ButtonState.SaveLocallyPrimary,
|
||||
)
|
||||
verify { mockSettingRepository.defaultSaveOption }
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `initial button state should be SaveLocallyPrimary when sync is enabled and default save option is NONE`() {
|
||||
every {
|
||||
mockAuthenticatorRepository.sharedCodesStateFlow
|
||||
} returns MutableStateFlow(SharedVerificationCodesState.Success(emptyList()))
|
||||
every { mockSettingRepository.defaultSaveOption } returns DefaultSaveOption.NONE
|
||||
|
||||
val viewModel = createViewModel(initialState = null)
|
||||
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
buttonState = ManualCodeEntryState.ButtonState.SaveToBitwardenPrimary,
|
||||
)
|
||||
verify { mockSettingRepository.defaultSaveOption }
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClick should navigate back`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
@@ -88,7 +153,7 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `CodeSubmit should createItem, show toast, and navigate back on success when code is valid`() =
|
||||
fun `SaveLocallyClick should createItem, show toast, and navigate back on success when code is valid`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
mockAuthenticatorRepository.createItem(
|
||||
@@ -109,7 +174,7 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
.copy(code = "ABCD", issuer = "mockIssuer"),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit)
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick)
|
||||
|
||||
coVerify {
|
||||
mockAuthenticatorRepository.createItem(
|
||||
@@ -136,8 +201,57 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `CodeSubmit should replace whitespace from code`() = runTest {
|
||||
fun `SaveToBitwardenClick should launch add to Bitwarden flow and navigate back on success when code is valid`() =
|
||||
runTest {
|
||||
val expectedUri = "otpauth://totp/?secret=ABCD&issuer=mockIssuer"
|
||||
every {
|
||||
mockAuthenticatorBridgeManager.startAddTotpLoginItemFlow(expectedUri)
|
||||
} returns true
|
||||
val viewModel = createViewModel(
|
||||
initialState = DEFAULT_STATE
|
||||
.copy(code = "ABCD", issuer = "mockIssuer"),
|
||||
)
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveToBitwardenClick)
|
||||
verify {
|
||||
mockAuthenticatorBridgeManager.startAddTotpLoginItemFlow(expectedUri)
|
||||
}
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
ManualCodeEntryEvent.NavigateBack,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `SaveToBitwardenClick should show error when code is valid but startAddTotpLoginItemFlow fails`() =
|
||||
runTest {
|
||||
val expectedUri = "otpauth://totp/?secret=ABCD&issuer=mockIssuer"
|
||||
every {
|
||||
mockAuthenticatorBridgeManager.startAddTotpLoginItemFlow(expectedUri)
|
||||
} returns false
|
||||
val viewModel = createViewModel(
|
||||
initialState = DEFAULT_STATE
|
||||
.copy(code = "ABCD", issuer = "mockIssuer"),
|
||||
)
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveToBitwardenClick)
|
||||
verify { mockAuthenticatorBridgeManager.startAddTotpLoginItemFlow(expectedUri) }
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
code = "ABCD",
|
||||
issuer = "mockIssuer",
|
||||
dialog = ManualCodeEntryState.DialogState.Error(
|
||||
title = R.string.something_went_wrong.asText(),
|
||||
message = R.string.please_try_again.asText(),
|
||||
),
|
||||
)
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveLocallyClick should replace whitespace from code`() = runTest {
|
||||
coEvery {
|
||||
mockAuthenticatorRepository.createItem(
|
||||
item = AuthenticatorItemEntity(
|
||||
@@ -159,7 +273,7 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit)
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick)
|
||||
|
||||
coVerify {
|
||||
mockAuthenticatorRepository.createItem(
|
||||
@@ -177,14 +291,14 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CodeSubmit should show error dialog when code is empty`() = runTest {
|
||||
fun `SaveLocallyClick should show error dialog when code is empty`() = runTest {
|
||||
val viewModel = createViewModel(
|
||||
initialState = DEFAULT_STATE.copy(
|
||||
code = " ",
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit)
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick)
|
||||
|
||||
assertEquals(
|
||||
ManualCodeEntryState.DialogState.Error(
|
||||
@@ -195,14 +309,14 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CodeSubmit should show error dialog when code is not base32`() {
|
||||
fun `SaveLocallyClick should show error dialog when code is not base32`() {
|
||||
val viewModel = createViewModel(
|
||||
initialState = DEFAULT_STATE.copy(
|
||||
code = "ABCD12345",
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit)
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick)
|
||||
|
||||
assertEquals(
|
||||
ManualCodeEntryState.DialogState.Error(
|
||||
@@ -213,7 +327,7 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CodeSubmit should show error dialog when issuer is empty`() {
|
||||
fun `SaveLocallyClick should show error dialog when issuer is empty`() {
|
||||
val viewModel = createViewModel(
|
||||
initialState = DEFAULT_STATE.copy(
|
||||
code = "ABCD",
|
||||
@@ -221,7 +335,7 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit)
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick)
|
||||
|
||||
assertEquals(
|
||||
ManualCodeEntryState.DialogState.Error(
|
||||
@@ -233,7 +347,7 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `CodeSubmit should set AuthenticatorItemType to STEAM when code starts with steam protocol`() {
|
||||
fun `SaveLocallyClick should set AuthenticatorItemType to STEAM when code starts with steam protocol`() {
|
||||
coEvery {
|
||||
mockAuthenticatorRepository.createItem(
|
||||
item = AuthenticatorItemEntity(
|
||||
@@ -255,7 +369,7 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(ManualCodeEntryAction.CodeSubmit)
|
||||
viewModel.trySendAction(ManualCodeEntryAction.SaveLocallyClick)
|
||||
|
||||
coVerify {
|
||||
mockAuthenticatorRepository.createItem(
|
||||
@@ -318,6 +432,8 @@ class ManualCodeEntryViewModelTest : BaseViewModelTest() {
|
||||
ManualCodeEntryViewModel(
|
||||
savedStateHandle = SavedStateHandle().apply { set("state", initialState) },
|
||||
authenticatorRepository = mockAuthenticatorRepository,
|
||||
authenticatorBridgeManager = mockAuthenticatorBridgeManager,
|
||||
settingsRepository = mockSettingRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -326,4 +442,5 @@ private val DEFAULT_STATE: ManualCodeEntryState =
|
||||
code = "",
|
||||
issuer = "",
|
||||
dialog = null,
|
||||
buttonState = ManualCodeEntryState.ButtonState.LocalOnly,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user