From 1446e43c464bfad818cf5ecc81eca4dd917970d7 Mon Sep 17 00:00:00 2001 From: Andrew Haisting <142518658+ahaisting-livefront@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:45:10 -0500 Subject: [PATCH] BITAU-105 Add support for deep link to account security (#4063) --- app/src/main/AndroidManifest.xml | 5 ++ .../java/com/x8bit/bitwarden/MainViewModel.kt | 7 +++ .../manager/model/SpecialCircumstance.kt | 6 +++ .../feature/rootnav/RootNavViewModel.kt | 1 + .../feature/settings/SettingsScreen.kt | 1 + .../feature/settings/SettingsViewModel.kt | 18 +++++++ .../VaultUnlockedNavBarScreen.kt | 6 ++- .../VaultUnlockedNavBarViewModel.kt | 11 ++++ .../ui/platform/util/ShortcutUtils.kt | 6 +++ .../com/x8bit/bitwarden/MainViewModelTest.kt | 53 +++++++++++++++++++ .../feature/rootnav/RootNavViewModelTest.kt | 38 +++++++++++++ .../feature/settings/SettingsScreenTest.kt | 19 +++++++ .../feature/settings/SettingsViewModelTest.kt | 45 +++++++++++++--- .../VaultUnlockedNavBarScreenTest.kt | 15 ++++++ .../VaultUnlockedNavBarViewModelTest.kt | 21 ++++++++ 15 files changed, 243 insertions(+), 9 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e36381a54..96675e7a1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -98,6 +98,11 @@ + + + + + { specialCircumstanceManager.specialCircumstance = SpecialCircumstance.VaultShortcut } + + hasAccountSecurityShortcut -> { + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.AccountSecurityShortcut + } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt index 2bcd57fe1..969115a43 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt @@ -98,6 +98,12 @@ sealed class SpecialCircumstance : Parcelable { @Parcelize data object VaultShortcut : SpecialCircumstance() + /** + * The app was launched via deeplink to the account security screen. + */ + @Parcelize + data object AccountSecurityShortcut : SpecialCircumstance() + /** * A subset of [SpecialCircumstance] that are only relevant in a pre-login state and should be * cleared after a successful login. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index ca13a34a6..74716bcaf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -151,6 +151,7 @@ class RootNavViewModel @Inject constructor( ) } + SpecialCircumstance.AccountSecurityShortcut, SpecialCircumstance.GeneratorShortcut, SpecialCircumstance.VaultShortcut, null, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt index 3991c3d94..e3238ff55 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreen.kt @@ -67,6 +67,7 @@ fun SettingsScreen( SettingsEvent.NavigateAutoFill -> onNavigateToAutoFill() SettingsEvent.NavigateOther -> onNavigateToOther() SettingsEvent.NavigateVault -> onNavigateToVault() + SettingsEvent.NavigateAccountSecurityShortcut -> onNavigateToAccountSecurity() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt index 394f808ad..c3af8d010 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt @@ -3,8 +3,11 @@ package com.x8bit.bitwarden.ui.platform.feature.settings import androidx.compose.material3.Text import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager +import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModel +import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.asText import dagger.hilt.android.lifecycle.HiltViewModel @@ -20,6 +23,7 @@ import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( settingsRepository: SettingsRepository, + specialCircumstanceManager: SpecialCircumstanceManager, ) : BaseViewModel( initialState = SettingsState( securityCount = settingsRepository.allSecuritySettingsBadgeCountFlow.value, @@ -39,6 +43,15 @@ class SettingsViewModel @Inject constructor( } .onEach(::sendAction) .launchIn(viewModelScope) + + when (specialCircumstanceManager.specialCircumstance) { + SpecialCircumstance.AccountSecurityShortcut -> { + sendEvent(SettingsEvent.NavigateAccountSecurityShortcut) + specialCircumstanceManager.specialCircumstance = null + } + + else -> Unit + } } override fun handleAction(action: SettingsAction): Unit = when (action) { @@ -115,6 +128,11 @@ sealed class SettingsEvent { */ data object NavigateAccountSecurity : SettingsEvent() + /** + * Navigate to the account security screen. + */ + data object NavigateAccountSecurityShortcut : SettingsEvent(), BackgroundEvent + /** * Navigate to the appearance screen. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index 7b01a79d8..a447e99d6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -58,7 +58,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType /** * Top level composable for the Vault Unlocked Screen. */ -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LongMethod") @Composable fun VaultUnlockedNavBarScreen( viewModel: VaultUnlockedNavBarViewModel = hiltViewModel(), @@ -104,6 +104,10 @@ fun VaultUnlockedNavBarScreen( VaultUnlockedNavBarEvent.NavigateToSettingsScreen -> { navigateToSettingsGraph(navOptions) } + + VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen -> { + navigateToSettingsGraph(navOptions) + } } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt index fc6aa30e2..4b26f2d1d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModel.kt @@ -65,6 +65,10 @@ class VaultUnlockedNavBarViewModel @Inject constructor( specialCircumstancesManager.specialCircumstance = null } + SpecialCircumstance.AccountSecurityShortcut -> { + sendEvent(VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen) + } + else -> Unit } } @@ -283,5 +287,12 @@ sealed class VaultUnlockedNavBarEvent { contentDescriptionRes = contentDescRes, ) } + + /** + * Navigate to the Settings Screen. + */ + data object NavigateToSettingsScreen : Shortcut() { + override val tab: VaultUnlockedNavBarTab = VaultUnlockedNavBarTab.Settings() + } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/ShortcutUtils.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/ShortcutUtils.kt index d55f35379..7ffb005cc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/ShortcutUtils.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/ShortcutUtils.kt @@ -13,3 +13,9 @@ val Intent.isMyVaultShortcut: Boolean */ val Intent.isPasswordGeneratorShortcut: Boolean get() = dataString?.equals("bitwarden://password_generator") == true + +/** + * Returns `true` if the [Intent] is a deeplink to the account security screen, `false` otherwise. + */ +val Intent.isAccountSecurityShortcut: Boolean + get() = dataString?.equals("bitwarden://settings/account_security") == true diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index e23f5008a..b219e288e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -48,6 +48,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager +import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut import com.x8bit.bitwarden.ui.vault.model.TotpData @@ -131,6 +132,7 @@ class MainViewModelTest : BaseViewModelTest() { mockkStatic( Intent::isMyVaultShortcut, Intent::isPasswordGeneratorShortcut, + Intent::isAccountSecurityShortcut, ) } @@ -146,6 +148,7 @@ class MainViewModelTest : BaseViewModelTest() { unmockkStatic( Intent::isMyVaultShortcut, Intent::isPasswordGeneratorShortcut, + Intent::isAccountSecurityShortcut, ) } @@ -312,6 +315,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = mockIntent)) assertEquals( @@ -334,6 +338,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction( MainAction.ReceiveFirstIntent( @@ -363,6 +368,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction( MainAction.ReceiveFirstIntent( @@ -394,6 +400,7 @@ class MainViewModelTest : BaseViewModelTest() { every { getAutofillSelectionDataOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { authRepository.activeUserId } returns null @@ -430,6 +437,7 @@ class MainViewModelTest : BaseViewModelTest() { every { getAutofillSelectionDataOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { authRepository.activeUserId } returns "activeId" @@ -468,6 +476,7 @@ class MainViewModelTest : BaseViewModelTest() { every { getAutofillSelectionDataOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { authRepository.activeUserId } returns null @@ -508,6 +517,7 @@ class MainViewModelTest : BaseViewModelTest() { every { getAutofillSelectionDataOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { authRepository.activeUserId } returns null @@ -550,6 +560,7 @@ class MainViewModelTest : BaseViewModelTest() { every { getAutofillSelectionDataOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { authRepository.activeUserId } returns null @@ -589,6 +600,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction( MainAction.ReceiveFirstIntent( @@ -619,6 +631,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction( MainAction.ReceiveFirstIntent( @@ -706,6 +719,7 @@ class MainViewModelTest : BaseViewModelTest() { every { getCompleteRegistrationDataIntentOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null coEvery { @@ -744,6 +758,7 @@ class MainViewModelTest : BaseViewModelTest() { every { getCompleteRegistrationDataIntentOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null coEvery { @@ -818,6 +833,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns shareData every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction( MainAction.ReceiveNewIntent( @@ -847,6 +863,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction(MainAction.ReceiveNewIntent(intent = mockIntent)) assertEquals( @@ -869,6 +886,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction( MainAction.ReceiveNewIntent( @@ -898,6 +916,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction( MainAction.ReceiveNewIntent( @@ -928,6 +947,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false viewModel.trySendAction( MainAction.ReceiveNewIntent( @@ -955,6 +975,7 @@ class MainViewModelTest : BaseViewModelTest() { every { getCompleteRegistrationDataIntentOrNull() } returns null every { isMyVaultShortcut } returns true every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null @@ -969,6 +990,33 @@ class MainViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `on ReceiveNewIntent with account security deeplink data should set the special circumstance to AccountSecurityShortcut `() { + val viewModel = createViewModel() + val mockIntent = mockk { + every { getTotpDataOrNull() } returns null + every { getPasswordlessRequestDataIntentOrNull() } returns null + every { getAutofillSaveItemOrNull() } returns null + every { getAutofillSelectionDataOrNull() } returns null + every { getCompleteRegistrationDataIntentOrNull() } returns null + every { isMyVaultShortcut } returns false + every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns true + } + every { intentManager.getShareDataFromIntent(mockIntent) } returns null + + viewModel.trySendAction( + MainAction.ReceiveNewIntent( + intent = mockIntent, + ), + ) + assertEquals( + SpecialCircumstance.AccountSecurityShortcut, + specialCircumstanceManager.specialCircumstance, + ) + } + @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with a password generator deeplink data should set the special circumstance to GeneratorShortcut`() { @@ -981,6 +1029,7 @@ class MainViewModelTest : BaseViewModelTest() { every { getCompleteRegistrationDataIntentOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns true + every { isAccountSecurityShortcut } returns false } every { intentManager.getShareDataFromIntent(mockIntent) } returns null @@ -1070,6 +1119,7 @@ class MainViewModelTest : BaseViewModelTest() { every { intentManager.getShareDataFromIntent(mockIntent) } returns null every { mockIntent.isMyVaultShortcut } returns false every { mockIntent.isPasswordGeneratorShortcut } returns false + every { mockIntent.isAccountSecurityShortcut } returns false every { passwordlessRequestData.userId } returns "userId" viewModel.trySendAction( @@ -1152,6 +1202,7 @@ private fun createMockFido2RegistrationIntent( every { getCompleteRegistrationDataIntentOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } private fun createMockFido2AssertionIntent( @@ -1166,6 +1217,7 @@ private fun createMockFido2AssertionIntent( every { getCompleteRegistrationDataIntentOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } private fun createMockFido2GetCredentialsIntent( @@ -1183,6 +1235,7 @@ private fun createMockFido2GetCredentialsIntent( every { getCompleteRegistrationDataIntentOrNull() } returns null every { isMyVaultShortcut } returns false every { isPasswordGeneratorShortcut } returns false + every { isAccountSecurityShortcut } returns false } private val FIXED_CLOCK: Clock = Clock.fixed( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index 0764fbf84..06908a483 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -481,6 +481,44 @@ class RootNavViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `when the active user has an unlocked vault but there is an AccountSecurityShortcut special circumstance the nav state should be VaultUnlocked`() { + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.AccountSecurityShortcut + mutableUserStateFlow.tryEmit( + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = true, + needsPasswordReset = false, + isBiometricsEnabled = false, + organizations = emptyList(), + needsMasterPassword = false, + trustedDevice = null, + hasMasterPassword = true, + isUsingKeyConnector = false, + firstTimeState = UserState.FirstTimeState(false), + onboardingStatus = OnboardingStatus.COMPLETE, + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.VaultUnlocked(activeUserId = "activeUserId"), + viewModel.stateFlow.value, + ) + } + @Suppress("MaxLineLength") @Test fun `when the active user has an unlocked vault but the is a ShareNewSend special circumstance the nav state should be VaultUnlockedForNewSend`() { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreenTest.kt index 0b008c26a..61581709e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsScreenTest.kt @@ -260,6 +260,25 @@ class SettingsScreenTest : BaseComposeTest() { assertTrue(haveCalledNavigateToVault) } + @Test + fun `on NavigateAccountSecurityShortcut should call onNavigateToAccountSecurity`() { + var haveCalledNavigateToAccountSecurity = false + composeTestRule.setContent { + SettingsScreen( + viewModel = viewModel, + onNavigateToAbout = { }, + onNavigateToAccountSecurity = { haveCalledNavigateToAccountSecurity = true }, + onNavigateToAppearance = { }, + onNavigateToAutoFill = { }, + onNavigateToOther = { }, + onNavigateToVault = { + }, + ) + } + mutableEventFlow.tryEmit(SettingsEvent.NavigateAccountSecurityShortcut) + assertTrue(haveCalledNavigateToAccountSecurity) + } + @Test fun `Settings screen should show correct number of notification badges based on state`() { composeTestRule.setContent { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt index cff3ad698..d21b937dd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt @@ -1,10 +1,15 @@ package com.x8bit.bitwarden.ui.platform.feature.settings import app.cash.turbine.test +import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager +import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest 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 kotlinx.coroutines.test.runTest @@ -19,10 +24,13 @@ class SettingsViewModelTest : BaseViewModelTest() { every { allSecuritySettingsBadgeCountFlow } returns mutableSecurityBadgeCountFlow every { allAutofillSettingsBadgeCountFlow } returns mutableAutofillBadgeCountFlow } + private val specialCircumstanceManager: SpecialCircumstanceManager = mockk { + every { specialCircumstance } returns null + } @Test fun `on SettingsClick with ABOUT should emit NavigateAbout`() = runTest { - val viewModel = SettingsViewModel(settingsRepository = settingsRepository) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) assertEquals(SettingsEvent.NavigateAbout, awaitItem()) @@ -31,7 +39,7 @@ class SettingsViewModelTest : BaseViewModelTest() { @Test fun `on SettingsClick with ACCOUNT_SECURITY should emit NavigateAccountSecurity`() = runTest { - val viewModel = SettingsViewModel(settingsRepository = settingsRepository) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY)) assertEquals(SettingsEvent.NavigateAccountSecurity, awaitItem()) @@ -40,7 +48,7 @@ class SettingsViewModelTest : BaseViewModelTest() { @Test fun `on SettingsClick with APPEARANCE should emit NavigateAppearance`() = runTest { - val viewModel = SettingsViewModel(settingsRepository = settingsRepository) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) assertEquals(SettingsEvent.NavigateAppearance, awaitItem()) @@ -49,7 +57,7 @@ class SettingsViewModelTest : BaseViewModelTest() { @Test fun `on SettingsClick with AUTO_FILL should emit NavigateAutoFill`() = runTest { - val viewModel = SettingsViewModel(settingsRepository = settingsRepository) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) assertEquals(SettingsEvent.NavigateAutoFill, awaitItem()) @@ -58,7 +66,7 @@ class SettingsViewModelTest : BaseViewModelTest() { @Test fun `on SettingsClick with OTHER should emit NavigateOther`() = runTest { - val viewModel = SettingsViewModel(settingsRepository = settingsRepository) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) assertEquals(SettingsEvent.NavigateOther, awaitItem()) @@ -67,7 +75,7 @@ class SettingsViewModelTest : BaseViewModelTest() { @Test fun `on SettingsClick with VAULT should emit NavigateVault`() = runTest { - val viewModel = SettingsViewModel(settingsRepository = settingsRepository) + val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) assertEquals(SettingsEvent.NavigateVault, awaitItem()) @@ -78,7 +86,7 @@ class SettingsViewModelTest : BaseViewModelTest() { fun `initial state reflects the current state of the repository`() { mutableAutofillBadgeCountFlow.update { 1 } mutableSecurityBadgeCountFlow.update { 2 } - val viewModel = SettingsViewModel(settingsRepository = settingsRepository) + val viewModel = createViewModel() assertEquals( SettingsState( autoFillCount = 1, @@ -90,7 +98,7 @@ class SettingsViewModelTest : BaseViewModelTest() { @Test fun `State updates when repository emits new values for badge counts`() = runTest { - val viewModel = SettingsViewModel(settingsRepository = settingsRepository) + val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals( SettingsState( @@ -119,4 +127,25 @@ class SettingsViewModelTest : BaseViewModelTest() { ) } } + + @Test + @Suppress("MaxLineLength") + fun `init should send NavigateAccountSecurityShortcut when special circumstance is AccountSecurityShortcut`() = + runTest { + every { + specialCircumstanceManager.specialCircumstance + } returns SpecialCircumstance.AccountSecurityShortcut + every { specialCircumstanceManager.specialCircumstance = null } just runs + createViewModel().eventFlow.test { + assertEquals( + SettingsEvent.NavigateAccountSecurityShortcut, awaitItem(), + ) + } + verify { specialCircumstanceManager.specialCircumstance = null } + } + + private fun createViewModel() = SettingsViewModel( + settingsRepository = settingsRepository, + specialCircumstanceManager = specialCircumstanceManager, + ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt index 9f4f07db9..0999c3b57 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt @@ -103,6 +103,21 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { } } + @Test + fun `NavigateToSettingsScreen shortcut event should navigate to SettingsScreen`() { + mutableEventFlow.tryEmit(VaultUnlockedNavBarEvent.NavigateToSendScreen) + composeTestRule.runOnIdle { fakeNavHostController.assertCurrentRoute("send_graph") } + mutableEventFlow.tryEmit( + VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen, + ) + composeTestRule.runOnIdle { + fakeNavHostController.assertLastNavigation( + route = "settings_graph", + navOptions = expectedNavOptions, + ) + } + } + @Test fun `send tab click should send SendTabClick action`() { composeTestRule.onNodeWithText("Send").performClick() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt index 05fbaf24c..f658b8474 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarViewModelTest.kt @@ -80,6 +80,27 @@ class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `on init with AccountSecurityShortcut special circumstance should navigate to the settings screen with shortcut event`() = + runTest { + every { + specialCircumstancesManager.specialCircumstance + } returns SpecialCircumstance.AccountSecurityShortcut + + val viewModel = createViewModel() + + viewModel.eventFlow.test { + assertEquals( + VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen, + awaitItem(), + ) + } + verify(exactly = 1) { + specialCircumstancesManager.specialCircumstance + } + } + @Test fun `on init with no shortcut special circumstance should do nothing`() = runTest { every { specialCircumstancesManager.specialCircumstance } returns null