From fed6b7480007c872a37d853c8978ec12b969e164 Mon Sep 17 00:00:00 2001 From: Sean Weiser <125889608+sean-livefront@users.noreply.github.com> Date: Thu, 18 Jan 2024 09:43:22 -0600 Subject: [PATCH] Save state of approving passwordless logins setting and setup initial UI (#644) --- .../datasource/disk/SettingsDiskSource.kt | 13 +++ .../datasource/disk/SettingsDiskSourceImpl.kt | 19 ++++ .../platform/repository/SettingsRepository.kt | 5 + .../repository/SettingsRepositoryImpl.kt | 14 +++ .../accountsecurity/AccountSecurityScreen.kt | 69 ++++++++++++-- .../AccountSecurityViewModel.kt | 56 ++++++++--- .../datasource/disk/SettingsDiskSourceTest.kt | 66 +++++++++++++ .../disk/util/FakeSettingsDiskSource.kt | 12 +++ .../repository/SettingsRepositoryTest.kt | 26 ++++++ .../AccountSecurityScreenTest.kt | 93 ++++++++++++++++++- .../AccountSecurityViewModelTest.kt | 82 +++++++++++++--- 11 files changed, 419 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt index 705df855b..2315b5ee1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt @@ -115,4 +115,17 @@ interface SettingsDiskSource { userId: String, blockedAutofillUris: List?, ) + + /** + * Gets whether or not the given [userId] has enabled approving passwordless logins. + */ + fun getApprovePasswordlessLoginsEnabled(userId: String): Boolean? + + /** + * Stores whether or not [isApprovePasswordlessLoginsEnabled] for the given [userId]. + */ + fun storeApprovePasswordlessLoginsEnabled( + userId: String, + isApprovePasswordlessLoginsEnabled: Boolean?, + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt index d84de3862..1216a0c8b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt @@ -20,6 +20,7 @@ private const val BLOCKED_AUTOFILL_URIS_KEY = "$BASE_KEY:autofillBlacklistedUris private const val VAULT_TIMEOUT_ACTION_KEY = "$BASE_KEY:vaultTimeoutAction" private const val VAULT_TIME_IN_MINUTES_KEY = "$BASE_KEY:vaultTimeout" private const val DISABLE_ICON_LOADING_KEY = "$BASE_KEY:disableFavicon" +private const val APPROVE_PASSWORDLESS_LOGINS_KEY = "$BASE_KEY:approvePasswordlessLogins" /** * Primary implementation of [SettingsDiskSource]. @@ -92,6 +93,10 @@ class SettingsDiskSourceImpl( storePullToRefreshEnabled(userId = userId, isPullToRefreshEnabled = null) storeInlineAutofillEnabled(userId = userId, isInlineAutofillEnabled = null) storeBlockedAutofillUris(userId = userId, blockedAutofillUris = null) + storeApprovePasswordlessLoginsEnabled( + userId = userId, + isApprovePasswordlessLoginsEnabled = null, + ) } override fun getVaultTimeoutInMinutes(userId: String): Int? = @@ -192,4 +197,18 @@ class SettingsDiskSourceImpl( mutablePullToRefreshEnabledFlowMap.getOrPut(userId) { bufferedMutableSharedFlow(replay = 1) } + + override fun getApprovePasswordlessLoginsEnabled(userId: String): Boolean? { + return getBoolean(key = "${APPROVE_PASSWORDLESS_LOGINS_KEY}_$userId") + } + + override fun storeApprovePasswordlessLoginsEnabled( + userId: String, + isApprovePasswordlessLoginsEnabled: Boolean?, + ) { + putBoolean( + key = "${APPROVE_PASSWORDLESS_LOGINS_KEY}_$userId", + value = isApprovePasswordlessLoginsEnabled, + ) + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt index 751160e11..c26834c0e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt @@ -62,6 +62,11 @@ interface SettingsRepository { */ var blockedAutofillUris: List + /** + * Whether or not approving passwordless logins is enabled for the current user. + */ + var isApprovePasswordlessLoginsEnabled: Boolean + /** * Sets default values for various settings for the given [userId] if necessary. This is * typically used when logging into a new account. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt index d88481865..f59cac7e4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt @@ -121,6 +121,20 @@ class SettingsRepositoryImpl( ) } + override var isApprovePasswordlessLoginsEnabled: Boolean + get() = activeUserId + ?.let { + settingsDiskSource.getApprovePasswordlessLoginsEnabled(it) + } + ?: false + set(value) { + val userId = activeUserId ?: return + settingsDiskSource.storeApprovePasswordlessLoginsEnabled( + userId = userId, + isApprovePasswordlessLoginsEnabled = value, + ) + } + override fun setDefaultsIfNecessary(userId: String) { // Set Vault Settings defaults if (!isVaultTimeoutActionSet(userId = userId)) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt index 991b8638b..6e3c772f6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt @@ -139,13 +139,10 @@ fun AccountSecurityScreen( .fillMaxWidth() .padding(horizontal = 16.dp), ) - BitwardenWideSwitch( - label = stringResource( - id = R.string.use_this_device_to_approve_login_requests_made_from_other_devices, - ), - isChecked = state.isApproveLoginRequestsEnabled, - onCheckedChange = remember(viewModel) { - { viewModel.trySendAction(AccountSecurityAction.LoginRequestToggle(it)) } + ApprovePasswordlessLoginsRow( + isApproveLoginRequestsEnabled = state.isApproveLoginRequestsEnabled, + onApprovePasswordlessLoginsAction = remember(viewModel) { + { viewModel.trySendAction(it) } }, modifier = Modifier .fillMaxWidth() @@ -617,3 +614,61 @@ private fun FingerPrintPhraseDialog( containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, ) } + +@Composable +private fun ApprovePasswordlessLoginsRow( + isApproveLoginRequestsEnabled: Boolean, + @Suppress("MaxLineLength") + onApprovePasswordlessLoginsAction: (AccountSecurityAction.ApprovePasswordlessLoginsToggle) -> Unit, + modifier: Modifier = Modifier, +) { + var shouldShowConfirmationDialog by remember { mutableStateOf(false) } + BitwardenWideSwitch( + label = stringResource( + id = R.string.use_this_device_to_approve_login_requests_made_from_other_devices, + ), + isChecked = isApproveLoginRequestsEnabled, + onCheckedChange = { isChecked -> + if (isChecked) { + onApprovePasswordlessLoginsAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.PendingEnabled, + ) + shouldShowConfirmationDialog = true + } else { + onApprovePasswordlessLoginsAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled, + ) + } + }, + modifier = modifier, + ) + + if (shouldShowConfirmationDialog) { + BitwardenTwoButtonDialog( + title = stringResource(id = R.string.approve_login_requests), + message = stringResource( + id = R.string.use_this_device_to_approve_login_requests_made_from_other_devices, + ), + confirmButtonText = stringResource(id = R.string.yes), + dismissButtonText = stringResource(id = R.string.no), + onConfirmClick = { + onApprovePasswordlessLoginsAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Enabled, + ) + shouldShowConfirmationDialog = false + }, + onDismissClick = { + onApprovePasswordlessLoginsAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled, + ) + shouldShowConfirmationDialog = false + }, + onDismissRequest = { + onApprovePasswordlessLoginsAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled, + ) + shouldShowConfirmationDialog = false + }, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt index 769be2d14..1080d07b9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt @@ -36,7 +36,7 @@ class AccountSecurityViewModel @Inject constructor( ?: AccountSecurityState( dialog = null, fingerprintPhrase = "fingerprint-placeholder".asText(), - isApproveLoginRequestsEnabled = false, + isApproveLoginRequestsEnabled = settingsRepository.isApprovePasswordlessLoginsEnabled, isUnlockWithBiometricsEnabled = false, isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled, vaultTimeout = settingsRepository.vaultTimeout, @@ -59,7 +59,6 @@ class AccountSecurityViewModel @Inject constructor( AccountSecurityAction.DismissDialog -> handleDismissDialog() AccountSecurityAction.FingerPrintLearnMoreClick -> handleFingerPrintLearnMoreClick() AccountSecurityAction.LockNowClick -> handleLockNowClick() - is AccountSecurityAction.LoginRequestToggle -> handleLoginRequestToggle(action) AccountSecurityAction.LogoutClick -> handleLogoutClick() AccountSecurityAction.PendingLoginRequestsClick -> handlePendingLoginRequestsClick() is AccountSecurityAction.VaultTimeoutTypeSelect -> handleVaultTimeoutTypeSelect(action) @@ -74,6 +73,10 @@ class AccountSecurityViewModel @Inject constructor( } is AccountSecurityAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action) + + is AccountSecurityAction.ApprovePasswordlessLoginsToggle -> { + handleApprovePasswordlessLoginsToggle(action) + } } private fun handleAccountFingerprintPhraseClick() { @@ -108,9 +111,25 @@ class AccountSecurityViewModel @Inject constructor( vaultRepository.lockVaultForCurrentUser() } - private fun handleLoginRequestToggle(action: AccountSecurityAction.LoginRequestToggle) { - // TODO BIT-466: Persist pending login requests state - mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = action.enabled) } + private fun handleApprovePasswordlessLoginsToggle( + action: AccountSecurityAction.ApprovePasswordlessLoginsToggle, + ) { + when (action) { + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled -> { + settingsRepository.isApprovePasswordlessLoginsEnabled = false + mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = false) } + } + + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Enabled -> { + settingsRepository.isApprovePasswordlessLoginsEnabled = true + mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = true) } + } + + AccountSecurityAction.ApprovePasswordlessLoginsToggle.PendingEnabled -> { + mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = true) } + } + } + // TODO Add permission prompt - BIT-1360 sendEvent(AccountSecurityEvent.ShowToast("Handle Login requests on this device.".asText())) } @@ -319,13 +338,6 @@ sealed class AccountSecurityAction { */ data object LockNowClick : AccountSecurityAction() - /** - * User toggled the login request switch. - */ - data class LoginRequestToggle( - val enabled: Boolean, - ) : AccountSecurityAction() - /** * User clicked log out. */ @@ -369,6 +381,26 @@ sealed class AccountSecurityAction { val enabled: Boolean, ) : AccountSecurityAction() + /** + * User toggled the approve passwordless logins switch. + */ + sealed class ApprovePasswordlessLoginsToggle : AccountSecurityAction() { + /** + * The toggle was enabled and confirmed. + */ + data object Enabled : ApprovePasswordlessLoginsToggle() + + /** + * The toggle was enabled but not yet confirmed. + */ + data object PendingEnabled : ApprovePasswordlessLoginsToggle() + + /** + * The toggle was disabled. + */ + data object Disabled : ApprovePasswordlessLoginsToggle() + } + /** * User toggled the unlock with pin switch. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt index 003b0da99..fd42e3fd1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt @@ -85,6 +85,10 @@ class SettingsDiskSourceTest { userId = userId, blockedAutofillUris = listOf("www.example.com"), ) + settingsDiskSource.storeApprovePasswordlessLoginsEnabled( + userId = userId, + isApprovePasswordlessLoginsEnabled = true, + ) settingsDiskSource.clearData(userId = userId) @@ -93,6 +97,7 @@ class SettingsDiskSourceTest { assertNull(settingsDiskSource.getPullToRefreshEnabled(userId = userId)) assertNull(settingsDiskSource.getInlineAutofillEnabled(userId = userId)) assertNull(settingsDiskSource.getBlockedAutofillUris(userId = userId)) + assertNull(settingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = userId)) } @Test @@ -507,4 +512,65 @@ class SettingsDiskSourceTest { json.parseToJsonElement(requireNotNull(actual)), ) } + + @Suppress("MaxLineLength") + @Test + fun `getApprovePasswordlessLoginsEnabled when values are present should pull from SharedPreferences`() { + val approvePasswordlessLoginsBaseKey = "bwPreferencesStorage:approvePasswordlessLogins" + val mockUserId = "mockUserId" + val isEnabled = true + fakeSharedPreferences + .edit { + putBoolean( + "${approvePasswordlessLoginsBaseKey}_$mockUserId", + isEnabled, + ) + } + val actual = settingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = mockUserId) + assertEquals( + isEnabled, + actual, + ) + } + + @Test + fun `getApprovePasswordlessLoginsEnabled when values are absent should return null`() { + val mockUserId = "mockUserId" + assertNull(settingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = mockUserId)) + } + + @Suppress("MaxLineLength") + @Test + fun `storeApprovePasswordlessLoginsEnabled for non-null values should update SharedPreferences`() { + val approvePasswordlessLoginsBaseKey = "bwPreferencesStorage:approvePasswordlessLogins" + val mockUserId = "mockUserId" + val isEnabled = true + settingsDiskSource.storeApprovePasswordlessLoginsEnabled( + userId = mockUserId, + isApprovePasswordlessLoginsEnabled = isEnabled, + ) + val actual = fakeSharedPreferences.getBoolean( + "${approvePasswordlessLoginsBaseKey}_$mockUserId", + false, + ) + assertEquals( + isEnabled, + actual, + ) + } + + @Test + fun `storeApprovePasswordlessLoginsEnabled for null values should clear SharedPreferences`() { + val approvePasswordlessLoginsBaseKey = "bwPreferencesStorage:approvePasswordlessLogins" + val mockUserId = "mockUserId" + val approvePasswordlessLoginsKey = "${approvePasswordlessLoginsBaseKey}_$mockUserId" + fakeSharedPreferences.edit { + putBoolean(approvePasswordlessLoginsKey, true) + } + settingsDiskSource.storeApprovePasswordlessLoginsEnabled( + userId = mockUserId, + isApprovePasswordlessLoginsEnabled = null, + ) + assertFalse(fakeSharedPreferences.contains(approvePasswordlessLoginsKey)) + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt index 2ae6658f9..66c557ec1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt @@ -39,6 +39,8 @@ class FakeSettingsDiskSource : SettingsDiskSource { private var storedIsIconLoadingDisabled: Boolean? = null + private val storedApprovePasswordLoginsEnabled = mutableMapOf() + override var appLanguage: AppLanguage? = null override var appTheme: AppTheme @@ -138,6 +140,16 @@ class FakeSettingsDiskSource : SettingsDiskSource { storedBlockedAutofillUris[userId] = blockedAutofillUris } + override fun getApprovePasswordlessLoginsEnabled(userId: String): Boolean? = + storedApprovePasswordLoginsEnabled[userId] + + override fun storeApprovePasswordlessLoginsEnabled( + userId: String, + isApprovePasswordlessLoginsEnabled: Boolean?, + ) { + storedApprovePasswordLoginsEnabled[userId] = isApprovePasswordlessLoginsEnabled + } + //region Private helper functions private fun getMutableVaultTimeoutActionsFlow( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt index 8c5ba19eb..4fb1bc4de 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt @@ -533,6 +533,32 @@ class SettingsRepositoryTest { ) } } + + @Test + fun `isApprovePasswordlessLoginsEnabled should properly update SettingsDiskSource`() { + fakeAuthDiskSource.userState = null + assertFalse(settingsRepository.isApprovePasswordlessLoginsEnabled) + + val userId = "userId" + fakeAuthDiskSource.userState = MOCK_USER_STATE + + // Updates to the disk source change the repository value + fakeSettingsDiskSource.storeApprovePasswordlessLoginsEnabled( + userId = userId, + isApprovePasswordlessLoginsEnabled = true, + ) + assertEquals( + true, + settingsRepository.isApprovePasswordlessLoginsEnabled, + ) + + // Updates to the repository value change the disk source + settingsRepository.isApprovePasswordlessLoginsEnabled = false + assertEquals( + false, + fakeSettingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = userId), + ) + } } private val MOCK_USER_STATE = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt index 116296669..803c85bf7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt @@ -69,13 +69,102 @@ class AccountSecurityScreenTest : BaseComposeTest() { verify { viewModel.trySendAction(AccountSecurityAction.LogoutClick) } } + @Suppress("MaxLineLength") @Test - fun `on approve login requests toggle should send LoginRequestToggle`() { + fun `on approve login requests toggle on should send PendingEnabled action and display dialog`() { + composeTestRule.assertNoDialogExists() + composeTestRule .onNodeWithText("Use this device to approve login requests made from other devices") .performScrollTo() .performClick() - verify { viewModel.trySendAction(AccountSecurityAction.LoginRequestToggle(true)) } + + composeTestRule + .onAllNodesWithText("Approve login requests") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + composeTestRule + .onAllNodesWithText( + "Use this device to approve login requests made from other devices", + ) + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + composeTestRule + .onAllNodesWithText("No") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + composeTestRule + .onAllNodesWithText("Yes") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + + verify { + viewModel.trySendAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.PendingEnabled, + ) + } + } + + @Test + fun `on approve login requests toggle off should send Disabled action`() { + mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = true) } + + composeTestRule + .onNodeWithText("Use this device to approve login requests made from other devices") + .performScrollTo() + .performClick() + + verify { + viewModel.trySendAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled, + ) + } + } + + @Test + fun `on approve login requests confirm Yes should send Enabled action and hide dialog`() { + mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = false) } + + composeTestRule + .onNodeWithText("Use this device to approve login requests made from other devices") + .performScrollTo() + .performClick() + + composeTestRule + .onAllNodesWithText("Yes") + .filterToOne(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule.assertNoDialogExists() + + verify { + viewModel.trySendAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Enabled, + ) + } + } + + @Test + fun `on approve login requests confirm No should send Disabled action and hide dialog`() { + mutableStateFlow.update { it.copy(isApproveLoginRequestsEnabled = false) } + + composeTestRule + .onNodeWithText("Use this device to approve login requests made from other devices") + .performScrollTo() + .performClick() + + composeTestRule + .onAllNodesWithText("No") + .filterToOne(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule.assertNoDialogExists() + + verify { + viewModel.trySendAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled, + ) + } } @Test diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt index f247e9637..99dc40023 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt @@ -16,6 +16,7 @@ import io.mockk.runs import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -33,6 +34,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { every { isUnlockWithPinEnabled } returns true every { vaultTimeout } returns VaultTimeout.ThirtyMinutes every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK + every { isApprovePasswordlessLoginsEnabled } returns false } val viewModel = createViewModel( initialState = null, @@ -112,21 +114,6 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { verify { vaultRepository.lockVaultForCurrentUser() } } - @Test - fun `on LoginRequestToggle should emit ShowToast`() = runTest { - val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(AccountSecurityAction.LoginRequestToggle(true)) - assertEquals( - AccountSecurityEvent.ShowToast("Handle Login requests on this device.".asText()), - awaitItem(), - ) - } - viewModel.stateFlow.test { - assertTrue(awaitItem().isApproveLoginRequestsEnabled) - } - } - @Test fun `on PendingLoginRequestsClick should emit ShowToast`() = runTest { val viewModel = createViewModel() @@ -323,6 +310,71 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { assertEquals(DEFAULT_STATE.copy(dialog = null), viewModel.stateFlow.value) } + @Suppress("MaxLineLength") + @Test + fun `on ApprovePasswordlessLoginsToggle enabled should update settings, set isApprovePasswordlessLoginsEnabled to true, and display toast`() = + runTest { + val settingsRepository = mockk { + every { isApprovePasswordlessLoginsEnabled = true } just runs + } + val viewModel = createViewModel( + settingsRepository = settingsRepository, + ) + viewModel.eventFlow.test { + viewModel.trySendAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Enabled, + ) + assertEquals( + AccountSecurityEvent.ShowToast("Handle Login requests on this device.".asText()), + awaitItem(), + ) + verify(exactly = 1) { settingsRepository.isApprovePasswordlessLoginsEnabled = true } + } + assertTrue(viewModel.stateFlow.value.isApproveLoginRequestsEnabled) + } + + @Suppress("MaxLineLength") + @Test + fun `on ApprovePasswordlessLoginsToggle pending enabled should set isApprovePasswordlessLoginsEnabled to true and display toast`() = + runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.PendingEnabled, + ) + assertEquals( + AccountSecurityEvent.ShowToast("Handle Login requests on this device.".asText()), + awaitItem(), + ) + } + assertTrue(viewModel.stateFlow.value.isApproveLoginRequestsEnabled) + } + + @Suppress("MaxLineLength") + @Test + fun `on ApprovePasswordlessLoginsToggle disabled should update settings, set isApprovePasswordlessLoginsEnabled to false, and display toast`() = + runTest { + val settingsRepository = mockk { + every { isApprovePasswordlessLoginsEnabled = false } just runs + } + val viewModel = createViewModel( + settingsRepository = settingsRepository, + ) + viewModel.eventFlow.test { + viewModel.trySendAction( + AccountSecurityAction.ApprovePasswordlessLoginsToggle.Disabled, + ) + assertEquals( + AccountSecurityEvent.ShowToast("Handle Login requests on this device.".asText()), + awaitItem(), + ) + verify(exactly = 1) { + settingsRepository.isApprovePasswordlessLoginsEnabled = false + } + } + assertFalse(viewModel.stateFlow.value.isApproveLoginRequestsEnabled) + } + private fun createViewModel( initialState: AccountSecurityState? = DEFAULT_STATE, authRepository: AuthRepository = mockk(relaxed = true),