BITAU-184 Allow user to save to Bitwarden when adding a code manually (#263)

This commit is contained in:
Andrew Haisting
2024-10-30 15:12:54 -05:00
committed by GitHub
parent bcc7e6756e
commit 5ac5e31dd2
6 changed files with 441 additions and 28 deletions

View File

@@ -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,

View File

@@ -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.

View File

@@ -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),
)
}
}
}
}

View File

@@ -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>

View File

@@ -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,
)

View File

@@ -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,
)