diff --git a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt index adb4d0303..cc462a0d2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt @@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut +import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged @@ -232,6 +233,7 @@ class MainViewModel @Inject constructor( val autofillSaveItem = intent.getAutofillSaveItemOrNull() val autofillSelectionData = intent.getAutofillSelectionDataOrNull() val shareData = intentManager.getShareDataFromIntent(intent) + val totpData = intent.getTotpDataOrNull() val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut val hasVaultShortcut = intent.isMyVaultShortcut val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull() @@ -270,6 +272,11 @@ class MainViewModel @Inject constructor( ) } + totpData != null -> { + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.AddTotpLoginItem(data = totpData) + } + shareData != null -> { specialCircumstanceManager.specialCircumstance = SpecialCircumstance.ShareNewSend( 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 f8c376f34..2bcd57fe1 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 @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager +import com.x8bit.bitwarden.ui.vault.model.TotpData import kotlinx.parcelize.Parcelize /** @@ -14,6 +15,14 @@ import kotlinx.parcelize.Parcelize * of navigation that is counter to what otherwise may happen based on the state of the app. */ sealed class SpecialCircumstance : Parcelable { + /** + * The app was launched in order to add a new TOTP to a cipher. + */ + @Parcelize + data class AddTotpLoginItem( + val data: TotpData, + ) : SpecialCircumstance() + /** * The app was launched in order to create/share a new Send using the given [data]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt index 02953510f..0e17e2b3a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt @@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import com.x8bit.bitwarden.ui.vault.model.TotpData /** * Returns [AutofillSaveItem] when contained in the given [SpecialCircumstance]. @@ -51,3 +52,12 @@ fun SpecialCircumstance.toFido2GetCredentialsRequestOrNull(): Fido2GetCredential is SpecialCircumstance.Fido2GetCredentials -> this.fido2GetCredentialsRequest else -> null } + +/** + * Returns the [TotpData] when contained in the given [SpecialCircumstance]. + */ +fun SpecialCircumstance.toTotpDataOrNull(): TotpData? = + when (this) { + is SpecialCircumstance.AddTotpLoginItem -> this.data + else -> null + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index ba452794a..fc063ca99 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -121,6 +121,7 @@ fun RootNavScreen( is RootNavState.VaultUnlockedForAutofillSave, is RootNavState.VaultUnlockedForAutofillSelection, is RootNavState.VaultUnlockedForNewSend, + is RootNavState.VaultUnlockedForNewTotp, is RootNavState.VaultUnlockedForAuthRequest, is RootNavState.VaultUnlockedForFido2Save, is RootNavState.VaultUnlockedForFido2Assertion, @@ -197,6 +198,14 @@ fun RootNavScreen( ) } + is RootNavState.VaultUnlockedForNewTotp -> { + navController.navigateToVaultUnlock(rootNavOptions) + navController.navigateToVaultItemListingAsRoot( + vaultItemListingType = VaultItemListingType.Login, + navOptions = rootNavOptions, + ) + } + is RootNavState.VaultUnlockedForAutofillSave -> { navController.navigateToVaultUnlockedGraph(rootNavOptions) navController.navigateToVaultAddEdit( 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 00c3c217d..5fc98402e 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 @@ -117,6 +117,12 @@ class RootNavViewModel @Inject constructor( ) } + is SpecialCircumstance.AddTotpLoginItem -> { + RootNavState.VaultUnlockedForNewTotp( + activeUserId = userState.activeAccount.userId, + ) + } + is SpecialCircumstance.ShareNewSend -> RootNavState.VaultUnlockedForNewSend is SpecialCircumstance.PasswordlessRequest -> { @@ -305,6 +311,14 @@ sealed class RootNavState : Parcelable { val fido2GetCredentialsRequest: Fido2GetCredentialsRequest, ) : RootNavState() + /** + * App should show the new verification codes listing screen for an unlocked user. + */ + @Parcelize + data class VaultUnlockedForNewTotp( + val activeUserId: String, + ) : RootNavState() + /** * App should show the new send screen for an unlocked user. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index 49353fb29..ceb6f4857 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -50,6 +50,8 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut +import com.x8bit.bitwarden.ui.vault.model.TotpData +import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull import io.mockk.coEvery import io.mockk.every import io.mockk.just @@ -119,6 +121,7 @@ class MainViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { mockkStatic( + Intent::getTotpDataOrNull, Intent::getPasswordlessRequestDataIntentOrNull, Intent::getAutofillSaveItemOrNull, Intent::getAutofillSelectionDataOrNull, @@ -134,6 +137,7 @@ class MainViewModelTest : BaseViewModelTest() { @AfterEach fun tearDown() { unmockkStatic( + Intent::getTotpDataOrNull, Intent::getPasswordlessRequestDataIntentOrNull, Intent::getAutofillSaveItemOrNull, Intent::getAutofillSelectionDataOrNull, @@ -294,12 +298,35 @@ class MainViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") + @Test + fun `on ReceiveFirstIntent with TOTP data should set the special circumstance to AddTotpLoginItem`() { + val viewModel = createViewModel() + val mockIntent = mockk() + val totpData = mockk() + every { mockIntent.getTotpDataOrNull() } returns totpData + every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null + every { mockIntent.getAutofillSaveItemOrNull() } returns null + every { mockIntent.getAutofillSelectionDataOrNull() } returns null + every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null + every { intentManager.getShareDataFromIntent(mockIntent) } returns null + every { mockIntent.isMyVaultShortcut } returns false + every { mockIntent.isPasswordGeneratorShortcut } returns false + + viewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = mockIntent)) + assertEquals( + SpecialCircumstance.AddTotpLoginItem(data = totpData), + specialCircumstanceManager.specialCircumstance, + ) + } + @Suppress("MaxLineLength") @Test fun `on ReceiveFirstIntent with share data should set the special circumstance to ShareNewSend`() { val viewModel = createViewModel() val mockIntent = mockk() val shareData = mockk() + every { mockIntent.getTotpDataOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null @@ -328,6 +355,7 @@ class MainViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() val mockIntent = mockk() val autofillSelectionData = mockk() + every { mockIntent.getTotpDataOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null @@ -359,6 +387,7 @@ class MainViewModelTest : BaseViewModelTest() { every { verificationToken } returns "token" } val mockIntent = mockk { + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData @@ -394,6 +423,7 @@ class MainViewModelTest : BaseViewModelTest() { every { verificationToken } returns "token" } val mockIntent = mockk { + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData @@ -431,6 +461,7 @@ class MainViewModelTest : BaseViewModelTest() { every { verificationToken } returns token } val mockIntent = mockk { + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData @@ -470,6 +501,7 @@ class MainViewModelTest : BaseViewModelTest() { every { verificationToken } returns token } val mockIntent = mockk { + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData @@ -511,6 +543,7 @@ class MainViewModelTest : BaseViewModelTest() { every { verificationToken } returns token } val mockIntent = mockk { + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData @@ -548,6 +581,7 @@ class MainViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() val mockIntent = mockk() val autofillSaveItem = mockk() + every { mockIntent.getTotpDataOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem every { mockIntent.getAutofillSelectionDataOrNull() } returns null @@ -578,6 +612,7 @@ class MainViewModelTest : BaseViewModelTest() { every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns passwordlessRequestData + every { mockIntent.getTotpDataOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null @@ -663,6 +698,7 @@ class MainViewModelTest : BaseViewModelTest() { origin = "mockOrigin", ) val mockIntent = mockk { + every { getTotpDataOrNull() } returns null every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null @@ -701,6 +737,7 @@ class MainViewModelTest : BaseViewModelTest() { ) val mockIntent = mockk { every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null @@ -773,6 +810,7 @@ class MainViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() val mockIntent = mockk() val shareData = mockk() + every { mockIntent.getTotpDataOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null @@ -795,12 +833,35 @@ class MainViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `on ReceiveNewIntent with TOTP data should set the special circumstance to AddTotpLoginItem`() { + val viewModel = createViewModel() + val mockIntent = mockk() + val totpData = mockk() + every { mockIntent.getTotpDataOrNull() } returns totpData + every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null + every { mockIntent.getAutofillSaveItemOrNull() } returns null + every { mockIntent.getAutofillSelectionDataOrNull() } returns null + every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null + every { intentManager.getShareDataFromIntent(mockIntent) } returns null + every { mockIntent.isMyVaultShortcut } returns false + every { mockIntent.isPasswordGeneratorShortcut } returns false + + viewModel.trySendAction(MainAction.ReceiveNewIntent(intent = mockIntent)) + assertEquals( + SpecialCircumstance.AddTotpLoginItem(data = totpData), + specialCircumstanceManager.specialCircumstance, + ) + } + @Suppress("MaxLineLength") @Test fun `on ReceiveNewIntent with autofill data should set the special circumstance to AutofillSelection`() { val viewModel = createViewModel() val mockIntent = mockk() val autofillSelectionData = mockk() + every { mockIntent.getTotpDataOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null @@ -829,6 +890,7 @@ class MainViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() val mockIntent = mockk() val autofillSaveItem = mockk() + every { mockIntent.getTotpDataOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem every { mockIntent.getAutofillSelectionDataOrNull() } returns null @@ -859,6 +921,7 @@ class MainViewModelTest : BaseViewModelTest() { every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns passwordlessRequestData + every { mockIntent.getTotpDataOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null @@ -885,6 +948,7 @@ class MainViewModelTest : BaseViewModelTest() { fun `on ReceiveNewIntent with a Vault deeplink data should set the special circumstance to VaultShortcut`() { val viewModel = createViewModel() val mockIntent = mockk { + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null @@ -910,6 +974,7 @@ class MainViewModelTest : BaseViewModelTest() { fun `on ReceiveNewIntent with a password generator deeplink data should set the special circumstance to GeneratorShortcut`() { val viewModel = createViewModel() val mockIntent = mockk { + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null @@ -1043,6 +1108,7 @@ private fun createMockFido2RegistrationIntent( fido2CredentialRequest: Fido2CredentialRequest = createMockFido2CredentialRequest(number = 1), ): Intent = mockk { every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null @@ -1056,6 +1122,7 @@ private fun createMockFido2AssertionIntent( createMockFido2CredentialAssertionRequest(number = 1), ): Intent = mockk { every { getFido2AssertionRequestOrNull() } returns fido2CredentialAssertionRequest + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null @@ -1070,6 +1137,7 @@ private fun createMockFido2GetCredentialsIntent( ), ): Intent = mockk { every { getFido2GetCredentialsRequestOrNull() } returns fido2GetCredentialsRequest + every { getTotpDataOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt index 22fa1e7ee..dfd0978e5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensionsTest.kt @@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentia import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import com.x8bit.bitwarden.ui.vault.model.TotpData import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull @@ -38,6 +39,7 @@ class SpecialCircumstanceExtensionsTest { data = mockk(), shouldFinishWhenComplete = true, ), + mockk(), SpecialCircumstance.PasswordlessRequest( passwordlessRequestData = mockk(), shouldFinishWhenComplete = true, @@ -87,6 +89,7 @@ class SpecialCircumstanceExtensionsTest { data = mockk(), shouldFinishWhenComplete = true, ), + mockk(), SpecialCircumstance.PasswordlessRequest( passwordlessRequestData = mockk(), shouldFinishWhenComplete = true, @@ -118,6 +121,7 @@ class SpecialCircumstanceExtensionsTest { SpecialCircumstance.AutofillSave( autofillSaveItem = mockk(), ), + mockk(), SpecialCircumstance.ShareNewSend( data = mockk(), shouldFinishWhenComplete = true, @@ -188,6 +192,7 @@ class SpecialCircumstanceExtensionsTest { data = mockk(), shouldFinishWhenComplete = true, ), + mockk(), SpecialCircumstance.PasswordlessRequest( passwordlessRequestData = mockk(), shouldFinishWhenComplete = true, @@ -234,6 +239,7 @@ class SpecialCircumstanceExtensionsTest { data = mockk(), shouldFinishWhenComplete = true, ), + mockk(), SpecialCircumstance.PasswordlessRequest( passwordlessRequestData = mockk(), shouldFinishWhenComplete = true, @@ -251,4 +257,31 @@ class SpecialCircumstanceExtensionsTest { assertNull(specialCircumstance.toFido2GetCredentialsRequestOrNull()) } } + + @Test + fun `toTotpDataOrNull should return a non-null value for AddTotpLoginItem`() { + val totpData = mockk() + assertEquals( + totpData, + SpecialCircumstance.AddTotpLoginItem(data = totpData).toTotpDataOrNull(), + ) + } + + @Test + fun `toTotpDataOrNull should return a null value for other types`() { + listOf( + mockk(), + mockk(), + mockk(), + mockk(), + mockk(), + mockk(), + mockk(), + SpecialCircumstance.GeneratorShortcut, + SpecialCircumstance.VaultShortcut, + ) + .forEach { specialCircumstance -> + assertNull(specialCircumstance.toTotpDataOrNull()) + } + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt index b96f406d3..c73327888 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt @@ -117,7 +117,7 @@ class FakeNavHostController : NavHostController(context = mockk()) { * Asserts the [currentRoute] matches the given [route]. */ fun assertCurrentRoute(route: String) { - assertEquals(currentRoute, route) + assertEquals(route, currentRoute) } /** @@ -128,16 +128,16 @@ class FakeNavHostController : NavHostController(context = mockk()) { navOptions: NavOptions? = null, navigatorExtras: Navigator.Extras? = null, ) { - assertEquals(currentRoute, route) - assertEquals(lastNavigation?.navOptions, navOptions) - assertEquals(lastNavigation?.navigatorExtras, navigatorExtras) + assertEquals(route, currentRoute) + assertEquals(navOptions, lastNavigation?.navOptions) + assertEquals(navigatorExtras, lastNavigation?.navigatorExtras) } /** * Asserts the [lastNavigation] includes the given [navOptions]. */ fun assertLastNavOptions(navOptions: NavOptions?) { - assertEquals(lastNavigation?.navOptions, navOptions) + assertEquals(navOptions, lastNavigation?.navOptions) } data class Navigation( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt index c2f87357a..2a8eed45e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt @@ -151,6 +151,15 @@ class RootNavScreenTest : BaseComposeTest() { ) } + // Make sure navigating to vault unlocked for new totp works as expected: + rootNavStateFlow.value = RootNavState.VaultUnlockedForNewTotp(activeUserId = "userId") + composeTestRule.runOnIdle { + fakeNavHostController.assertLastNavigation( + route = "vault_item_listing_as_root/login", + navOptions = expectedNavOptions, + ) + } + // Make sure navigating to vault unlocked for new sends works as expected: rootNavStateFlow.value = RootNavState.VaultUnlockedForNewSend composeTestRule.runOnIdle { 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 c691b434d..001ac1313 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 @@ -414,6 +414,43 @@ class RootNavViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `when the active user has an unlocked vault but there is an AddTotpLoginItem special circumstance the nav state should be VaultUnlockedForNewTotp`() { + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.AddTotpLoginItem(data = mockk()) + 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, + onboardingStatus = OnboardingStatus.COMPLETE, + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.VaultUnlockedForNewTotp(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`() {