mirror of
https://github.com/bitwarden/android.git
synced 2024-11-22 01:16:02 +03:00
BITAU-107 Add Feature Flag and UI for Authenticator Syncing (#3847)
This commit is contained in:
parent
69ca7649e2
commit
b1ecf125d1
13 changed files with 243 additions and 13 deletions
|
@ -25,6 +25,7 @@ sealed class FlagKey<out T : Any> {
|
|||
*/
|
||||
val activeFlags: List<FlagKey<*>> by lazy {
|
||||
listOf(
|
||||
AuthenticatorSync,
|
||||
EmailVerification,
|
||||
OnboardingFlow,
|
||||
OnboardingCarousel,
|
||||
|
@ -32,6 +33,15 @@ sealed class FlagKey<out T : Any> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for syncing with the Bitwarden Authenticator app.
|
||||
*/
|
||||
data object AuthenticatorSync : FlagKey<Boolean>() {
|
||||
override val keyName: String = "authenticator-sync"
|
||||
override val defaultValue: Boolean = false
|
||||
override val isRemotelyConfigured: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the key for Email Verification feature.
|
||||
*/
|
||||
|
|
|
@ -37,6 +37,11 @@ interface SettingsRepository {
|
|||
*/
|
||||
var initialAutofillDialogShown: Boolean
|
||||
|
||||
/**
|
||||
* Whether the user has enabled syncing with the Bitwarden Authenticator app.
|
||||
*/
|
||||
var isAuthenticatorSyncEnabled: Boolean
|
||||
|
||||
/**
|
||||
* The currently stored last time the vault was synced.
|
||||
*/
|
||||
|
|
|
@ -99,6 +99,9 @@ class SettingsRepositoryImpl(
|
|||
}
|
||||
?: MutableStateFlow(value = null)
|
||||
|
||||
// TODO: this should be backed by disk and should set and clear the sync key (BITAU-103)
|
||||
override var isAuthenticatorSyncEnabled: Boolean = false
|
||||
|
||||
override var isIconLoadingDisabled: Boolean
|
||||
get() = settingsDiskSource.isIconLoadingDisabled ?: false
|
||||
set(value) {
|
||||
|
|
|
@ -28,13 +28,15 @@ class DebugMenuViewModel @Inject constructor(
|
|||
|
||||
init {
|
||||
combine(
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.AuthenticatorSync),
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.EmailVerification),
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingCarousel),
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
|
||||
) { (emailVerification, onboardingCarousel, onboardingFlow) ->
|
||||
) { (authenticatorSync, emailVerification, onboardingCarousel, onboardingFlow) ->
|
||||
sendAction(
|
||||
DebugMenuAction.Internal.UpdateFeatureFlagMap(
|
||||
mapOf(
|
||||
FlagKey.AuthenticatorSync to authenticatorSync,
|
||||
FlagKey.EmailVerification to emailVerification,
|
||||
FlagKey.OnboardingCarousel to onboardingCarousel,
|
||||
FlagKey.OnboardingFlow to onboardingFlow,
|
||||
|
|
|
@ -22,6 +22,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
|
|||
FlagKey.DummyString,
|
||||
-> Unit
|
||||
|
||||
FlagKey.AuthenticatorSync,
|
||||
FlagKey.EmailVerification,
|
||||
FlagKey.OnboardingCarousel,
|
||||
FlagKey.OnboardingFlow,
|
||||
|
@ -62,6 +63,7 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
|||
FlagKey.DummyString,
|
||||
-> this.keyName
|
||||
|
||||
FlagKey.AuthenticatorSync -> stringResource(R.string.authenticator_sync)
|
||||
FlagKey.EmailVerification -> stringResource(R.string.email_verification)
|
||||
FlagKey.OnboardingCarousel -> stringResource(R.string.onboarding_carousel)
|
||||
FlagKey.OnboardingFlow -> stringResource(R.string.onboarding_flow)
|
||||
|
|
|
@ -54,6 +54,7 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
|||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenPolicyWarningText
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithBiometricsSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithPinSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
|
@ -71,7 +72,7 @@ private const val MINUTES_PER_HOUR = 60
|
|||
/**
|
||||
* Displays the account security screen.
|
||||
*/
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountSecurityScreen(
|
||||
|
@ -229,6 +230,19 @@ fun AccountSecurityScreen(
|
|||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
if (state.shouldShowEnableAuthenticatorSync) {
|
||||
SyncWithAuthenticatorRow(
|
||||
isChecked = state.isAuthenticatorSyncChecked,
|
||||
onCheckedChange = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.AuthenticatorSyncToggle(enabled = it),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.session_timeout),
|
||||
modifier = Modifier
|
||||
|
@ -645,6 +659,27 @@ private fun SessionTimeoutActionRow(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SyncWithAuthenticatorRow(
|
||||
isChecked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(R.string.authenticator_sync),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
BitwardenWideSwitch(
|
||||
label = stringResource(R.string.allow_bitwarden_authenticator_syncing),
|
||||
onCheckedChange = onCheckedChange,
|
||||
isChecked = isChecked,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FingerPrintPhraseDialog(
|
||||
fingerprintPhrase: Text,
|
||||
|
|
|
@ -9,7 +9,9 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
|
@ -45,6 +47,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
private val settingsRepository: SettingsRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
policyManager: PolicyManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<AccountSecurityState, AccountSecurityEvent, AccountSecurityAction>(
|
||||
|
@ -57,6 +60,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
AccountSecurityState(
|
||||
dialog = null,
|
||||
fingerprintPhrase = "".asText(), // This will be filled in dynamically
|
||||
isAuthenticatorSyncChecked = settingsRepository.isAuthenticatorSyncEnabled,
|
||||
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled &&
|
||||
isBiometricsValid,
|
||||
isUnlockWithPasswordEnabled = authRepository
|
||||
|
@ -65,6 +69,9 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
?.activeAccount
|
||||
?.hasMasterPassword != false,
|
||||
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
|
||||
shouldShowEnableAuthenticatorSync = featureFlagManager.getFeatureFlag(
|
||||
key = FlagKey.AuthenticatorSync,
|
||||
),
|
||||
userId = userId,
|
||||
vaultTimeout = settingsRepository.vaultTimeout,
|
||||
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
|
||||
|
@ -99,6 +106,13 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
featureFlagManager
|
||||
.getFeatureFlagFlow(FlagKey.AuthenticatorSync)
|
||||
.onEach {
|
||||
trySendAction(AccountSecurityAction.Internal.AuthenticatorSyncFeatureFlagUpdate(it))
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
AccountSecurityAction.Internal.FingerprintResultReceive(
|
||||
|
@ -110,6 +124,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
|
||||
override fun handleAction(action: AccountSecurityAction): Unit = when (action) {
|
||||
AccountSecurityAction.AccountFingerprintPhraseClick -> handleAccountFingerprintPhraseClick()
|
||||
is AccountSecurityAction.AuthenticatorSyncToggle -> handleAuthenticatorSyncToggle(action)
|
||||
AccountSecurityAction.BackClick -> handleBackClick()
|
||||
AccountSecurityAction.ChangeMasterPasswordClick -> handleChangeMasterPasswordClick()
|
||||
AccountSecurityAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
|
||||
|
@ -137,6 +152,13 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.FingerprintPhrase) }
|
||||
}
|
||||
|
||||
private fun handleAuthenticatorSyncToggle(
|
||||
action: AccountSecurityAction.AuthenticatorSyncToggle,
|
||||
) {
|
||||
settingsRepository.isAuthenticatorSyncEnabled = action.enabled
|
||||
mutableStateFlow.update { it.copy(isAuthenticatorSyncChecked = action.enabled) }
|
||||
}
|
||||
|
||||
private fun handleBackClick() = sendEvent(AccountSecurityEvent.NavigateBack)
|
||||
|
||||
private fun handleChangeMasterPasswordClick() {
|
||||
|
@ -292,6 +314,10 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
|
||||
private fun handleInternalAction(action: AccountSecurityAction.Internal) {
|
||||
when (action) {
|
||||
is AccountSecurityAction.Internal.AuthenticatorSyncFeatureFlagUpdate -> {
|
||||
handleAuthenticatorSyncFeatureFlagUpdate(action)
|
||||
}
|
||||
|
||||
is AccountSecurityAction.Internal.BiometricsKeyResultReceive -> {
|
||||
handleBiometricsKeyResultReceive(action)
|
||||
}
|
||||
|
@ -306,6 +332,16 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleAuthenticatorSyncFeatureFlagUpdate(
|
||||
action: AccountSecurityAction.Internal.AuthenticatorSyncFeatureFlagUpdate,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
shouldShowEnableAuthenticatorSync = action.isEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBiometricsKeyResultReceive(
|
||||
action: AccountSecurityAction.Internal.BiometricsKeyResultReceive,
|
||||
) {
|
||||
|
@ -377,9 +413,11 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
data class AccountSecurityState(
|
||||
val dialog: AccountSecurityDialog?,
|
||||
val fingerprintPhrase: Text,
|
||||
val isAuthenticatorSyncChecked: Boolean,
|
||||
val isUnlockWithBiometricsEnabled: Boolean,
|
||||
val isUnlockWithPasswordEnabled: Boolean,
|
||||
val isUnlockWithPinEnabled: Boolean,
|
||||
val shouldShowEnableAuthenticatorSync: Boolean,
|
||||
val userId: String,
|
||||
val vaultTimeout: VaultTimeout,
|
||||
val vaultTimeoutAction: VaultTimeoutAction,
|
||||
|
@ -493,6 +531,13 @@ sealed class AccountSecurityAction {
|
|||
*/
|
||||
data object AccountFingerprintPhraseClick : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User clicked the authenticator sync toggle.
|
||||
*/
|
||||
data class AuthenticatorSyncToggle(
|
||||
val enabled: Boolean,
|
||||
) : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User clicked back button.
|
||||
*/
|
||||
|
@ -592,6 +637,14 @@ sealed class AccountSecurityAction {
|
|||
* Models actions that can be sent by the view model itself.
|
||||
*/
|
||||
sealed class Internal : AccountSecurityAction() {
|
||||
|
||||
/**
|
||||
* The feature flag value for authenticator syncing was updated.
|
||||
*/
|
||||
data class AuthenticatorSyncFeatureFlagUpdate(
|
||||
val isEnabled: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* A biometrics key result has been received.
|
||||
*/
|
||||
|
|
|
@ -992,4 +992,6 @@ Do you want to switch to this account?</string>
|
|||
<string name="expired_link">Expired link</string>
|
||||
<string name="please_restart_registration_or_try_logging_in">Please restart registration or try logging in. You may already have an account.</string>
|
||||
<string name="restart_registration">Restart registration</string>
|
||||
<string name="authenticator_sync">Authenticator Sync</string>
|
||||
<string name="allow_bitwarden_authenticator_syncing">Allow Bitwarden Authenticator Syncing</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FlagKeyTest {
|
||||
|
||||
@Test
|
||||
fun `AuthenticatorSync default value should be false`() {
|
||||
assertFalse(FlagKey.AuthenticatorSync.defaultValue)
|
||||
}
|
||||
}
|
|
@ -1014,6 +1014,18 @@ class SettingsRepositoryTest {
|
|||
settingsRepository.initialAutofillDialogShown = false
|
||||
assertEquals(false, fakeSettingsDiskSource.initialAutofillDialogShown)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isAuthenticatorSyncEnabled should default to false`() {
|
||||
assertFalse(settingsRepository.isAuthenticatorSyncEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isAuthenticatorSyncEnabled should be kept in memory and update accordingly`() = runTest {
|
||||
assertFalse(settingsRepository.isAuthenticatorSyncEnabled)
|
||||
settingsRepository.isAuthenticatorSyncEnabled = true
|
||||
assertTrue(settingsRepository.isAuthenticatorSyncEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_ID: String = "userId"
|
||||
|
|
|
@ -76,12 +76,14 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
private val DEFAULT_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
||||
FlagKey.AuthenticatorSync to true,
|
||||
FlagKey.EmailVerification to true,
|
||||
FlagKey.OnboardingCarousel to true,
|
||||
FlagKey.OnboardingFlow to true,
|
||||
)
|
||||
|
||||
private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
|
||||
FlagKey.AuthenticatorSync to false,
|
||||
FlagKey.EmailVerification to false,
|
||||
FlagKey.OnboardingCarousel to true,
|
||||
FlagKey.OnboardingFlow to false,
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.test.hasClickAction
|
|||
import androidx.compose.ui.test.hasTextExactly
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.isDisplayed
|
||||
import androidx.compose.ui.test.isToggleable
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
|
@ -74,6 +75,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
private val viewModel = mockk<AccountSecurityViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { trySendAction(any()) } just runs
|
||||
}
|
||||
|
||||
@Before
|
||||
|
@ -1457,6 +1459,35 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
|
||||
composeTestRule.onNodeWithText(rowText).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync with Bitwarden authenticator UI should be displayed according to state`() {
|
||||
val toggleText = "Allow Bitwarden Authenticator Syncing"
|
||||
composeTestRule.onNodeWithText(toggleText).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update { DEFAULT_STATE.copy(shouldShowEnableAuthenticatorSync = true) }
|
||||
composeTestRule.onNodeWithText(toggleText).performScrollTo().assertIsDisplayed()
|
||||
composeTestRule.onAllNodesWithText(toggleText).filterToOne(isToggleable()).assertIsOff()
|
||||
|
||||
mutableStateFlow.update {
|
||||
DEFAULT_STATE.copy(
|
||||
shouldShowEnableAuthenticatorSync = true,
|
||||
isAuthenticatorSyncChecked = true,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText(toggleText).assertIsDisplayed()
|
||||
composeTestRule.onAllNodesWithText(toggleText).filterToOne(isToggleable()).assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync with Bitwarden authenticator click should send AuthenticatorSyncToggle action`() {
|
||||
mutableStateFlow.update { DEFAULT_STATE.copy(shouldShowEnableAuthenticatorSync = true) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Allow Bitwarden Authenticator Syncing")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AccountSecurityAction.AuthenticatorSyncToggle(true)) }
|
||||
}
|
||||
}
|
||||
|
||||
private val CIPHER = mockk<Cipher>()
|
||||
|
@ -1464,10 +1495,12 @@ private const val USER_ID: String = "activeUserId"
|
|||
private val DEFAULT_STATE = AccountSecurityState(
|
||||
dialog = null,
|
||||
fingerprintPhrase = "fingerprint-placeholder".asText(),
|
||||
isAuthenticatorSyncChecked = false,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
isUnlockWithPasswordEnabled = true,
|
||||
isUnlockWithPinEnabled = false,
|
||||
userId = USER_ID,
|
||||
shouldShowEnableAuthenticatorSync = false,
|
||||
vaultTimeout = VaultTimeout.ThirtyMinutes,
|
||||
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
||||
vaultTimeoutPolicyMinutes = null,
|
||||
|
|
|
@ -8,7 +8,9 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
|
@ -24,6 +26,7 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
|||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.UnlockWithPinState
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.AccountSecurityAction.AuthenticatorSyncToggle
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
|
@ -32,6 +35,7 @@ import io.mockk.mockk
|
|||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
|
@ -49,6 +53,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
private val vaultRepository: VaultRepository = mockk(relaxed = true)
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { isAuthenticatorSyncEnabled } returns false
|
||||
every { isUnlockWithBiometricsEnabled } returns false
|
||||
every { isUnlockWithPinEnabled } returns false
|
||||
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
|
||||
|
@ -58,12 +63,22 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk {
|
||||
every { createCipherOrNull(DEFAULT_USER_STATE.activeUserId) } returns CIPHER
|
||||
every { getOrCreateCipher(DEFAULT_USER_STATE.activeUserId) } returns CIPHER
|
||||
every {
|
||||
isBiometricIntegrityValid(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
cipher = CIPHER,
|
||||
)
|
||||
} returns true
|
||||
}
|
||||
private val policyManager: PolicyManager = mockk {
|
||||
every {
|
||||
getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
|
||||
} returns mutableActivePolicyFlow
|
||||
}
|
||||
private val featureFlagManager: FeatureFlagManager = mockk(relaxed = true) {
|
||||
every { getFeatureFlag(FlagKey.AuthenticatorSync) } returns false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when saved state is set`() {
|
||||
|
@ -74,19 +89,9 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `initial state should be correct when saved state is not set`() {
|
||||
every { settingsRepository.isUnlockWithPinEnabled } returns true
|
||||
every {
|
||||
biometricsEncryptionManager.getOrCreateCipher(DEFAULT_USER_STATE.activeUserId)
|
||||
} returns CIPHER
|
||||
every {
|
||||
biometricsEncryptionManager.isBiometricIntegrityValid(
|
||||
userId = DEFAULT_USER_STATE.activeUserId,
|
||||
cipher = CIPHER,
|
||||
)
|
||||
} returns true
|
||||
val viewModel = createViewModel(initialState = null)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(isUnlockWithPinEnabled = true),
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
verify {
|
||||
|
@ -128,6 +133,20 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on AuthenticatorSyncToggle should update SettingsRepository and isAuthenticatorSyncChecked`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
every { settingsRepository.isAuthenticatorSyncEnabled = true } just runs
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AuthenticatorSyncToggle(enabled = true))
|
||||
assertEquals(DEFAULT_STATE.copy(isAuthenticatorSyncChecked = true), awaitItem())
|
||||
}
|
||||
verify { settingsRepository.isAuthenticatorSyncEnabled = true }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on FingerprintResultReceive should update the fingerprint phrase`() = runTest {
|
||||
val fingerprint = "fingerprint"
|
||||
|
@ -597,10 +616,46 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when featureFlagManger returns true for AuthenticatorSync, should show authenticator sync UI`() {
|
||||
val vm = createViewModel(
|
||||
initialState = null,
|
||||
featureFlagManager = mockk {
|
||||
every { getFeatureFlag(FlagKey.AuthenticatorSync) } returns true
|
||||
every { getFeatureFlagFlow(FlagKey.AuthenticatorSync) } returns emptyFlow()
|
||||
},
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(shouldShowEnableAuthenticatorSync = true),
|
||||
vm.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when featureFlagManger updates value AuthenticatorSync, should update UI`() = runTest {
|
||||
val featureFlagFlow = MutableStateFlow(false)
|
||||
val vm = createViewModel(
|
||||
initialState = null,
|
||||
featureFlagManager = mockk {
|
||||
every { getFeatureFlag(FlagKey.AuthenticatorSync) } returns false
|
||||
every { getFeatureFlagFlow(FlagKey.AuthenticatorSync) } returns featureFlagFlow
|
||||
},
|
||||
)
|
||||
vm.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
featureFlagFlow.value = true
|
||||
assertEquals(DEFAULT_STATE.copy(shouldShowEnableAuthenticatorSync = true), awaitItem())
|
||||
featureFlagFlow.value = false
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun createViewModel(
|
||||
initialState: AccountSecurityState? = DEFAULT_STATE,
|
||||
authRepository: AuthRepository = this.authRepository,
|
||||
featureFlagManager: FeatureFlagManager = this.featureFlagManager,
|
||||
vaultRepository: VaultRepository = this.vaultRepository,
|
||||
environmentRepository: EnvironmentRepository = this.fakeEnvironmentRepository,
|
||||
settingsRepository: SettingsRepository = this.settingsRepository,
|
||||
|
@ -608,6 +663,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
policyManager: PolicyManager = this.policyManager,
|
||||
): AccountSecurityViewModel = AccountSecurityViewModel(
|
||||
authRepository = authRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
vaultRepository = vaultRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
environmentRepository = environmentRepository,
|
||||
|
@ -648,6 +704,7 @@ private val DEFAULT_USER_STATE = UserState(
|
|||
private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState(
|
||||
dialog = null,
|
||||
fingerprintPhrase = FINGERPRINT.asText(),
|
||||
isAuthenticatorSyncChecked = false,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
isUnlockWithPasswordEnabled = true,
|
||||
isUnlockWithPinEnabled = false,
|
||||
|
@ -656,4 +713,5 @@ private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState(
|
|||
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
||||
vaultTimeoutPolicyMinutes = null,
|
||||
vaultTimeoutPolicyAction = null,
|
||||
shouldShowEnableAuthenticatorSync = false,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue