PM-13019: Add special circumstance to navigate to the vault listing UI for TOTP code (#4033)

This commit is contained in:
David Perez 2024-10-07 10:04:58 -05:00 committed by GitHub
parent 8d578a9b57
commit c4467f0cba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 201 additions and 5 deletions

View file

@ -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.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -232,6 +233,7 @@ class MainViewModel @Inject constructor(
val autofillSaveItem = intent.getAutofillSaveItemOrNull() val autofillSaveItem = intent.getAutofillSaveItemOrNull()
val autofillSelectionData = intent.getAutofillSelectionDataOrNull() val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
val shareData = intentManager.getShareDataFromIntent(intent) val shareData = intentManager.getShareDataFromIntent(intent)
val totpData = intent.getTotpDataOrNull()
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut val hasVaultShortcut = intent.isMyVaultShortcut
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull() val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
@ -270,6 +272,11 @@ class MainViewModel @Inject constructor(
) )
} }
totpData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AddTotpLoginItem(data = totpData)
}
shareData != null -> { shareData != null -> {
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ShareNewSend( SpecialCircumstance.ShareNewSend(

View file

@ -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.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.vault.model.TotpData
import kotlinx.parcelize.Parcelize 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. * of navigation that is counter to what otherwise may happen based on the state of the app.
*/ */
sealed class SpecialCircumstance : Parcelable { 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]. * The app was launched in order to create/share a new Send using the given [data].
*/ */

View file

@ -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.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance 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]. * Returns [AutofillSaveItem] when contained in the given [SpecialCircumstance].
@ -51,3 +52,12 @@ fun SpecialCircumstance.toFido2GetCredentialsRequestOrNull(): Fido2GetCredential
is SpecialCircumstance.Fido2GetCredentials -> this.fido2GetCredentialsRequest is SpecialCircumstance.Fido2GetCredentials -> this.fido2GetCredentialsRequest
else -> null else -> null
} }
/**
* Returns the [TotpData] when contained in the given [SpecialCircumstance].
*/
fun SpecialCircumstance.toTotpDataOrNull(): TotpData? =
when (this) {
is SpecialCircumstance.AddTotpLoginItem -> this.data
else -> null
}

View file

@ -121,6 +121,7 @@ fun RootNavScreen(
is RootNavState.VaultUnlockedForAutofillSave, is RootNavState.VaultUnlockedForAutofillSave,
is RootNavState.VaultUnlockedForAutofillSelection, is RootNavState.VaultUnlockedForAutofillSelection,
is RootNavState.VaultUnlockedForNewSend, is RootNavState.VaultUnlockedForNewSend,
is RootNavState.VaultUnlockedForNewTotp,
is RootNavState.VaultUnlockedForAuthRequest, is RootNavState.VaultUnlockedForAuthRequest,
is RootNavState.VaultUnlockedForFido2Save, is RootNavState.VaultUnlockedForFido2Save,
is RootNavState.VaultUnlockedForFido2Assertion, 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 -> { is RootNavState.VaultUnlockedForAutofillSave -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions) navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultAddEdit( navController.navigateToVaultAddEdit(

View file

@ -117,6 +117,12 @@ class RootNavViewModel @Inject constructor(
) )
} }
is SpecialCircumstance.AddTotpLoginItem -> {
RootNavState.VaultUnlockedForNewTotp(
activeUserId = userState.activeAccount.userId,
)
}
is SpecialCircumstance.ShareNewSend -> RootNavState.VaultUnlockedForNewSend is SpecialCircumstance.ShareNewSend -> RootNavState.VaultUnlockedForNewSend
is SpecialCircumstance.PasswordlessRequest -> { is SpecialCircumstance.PasswordlessRequest -> {
@ -305,6 +311,14 @@ sealed class RootNavState : Parcelable {
val fido2GetCredentialsRequest: Fido2GetCredentialsRequest, val fido2GetCredentialsRequest: Fido2GetCredentialsRequest,
) : RootNavState() ) : 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. * App should show the new send screen for an unlocked user.
*/ */

View file

@ -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.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut 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.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
@ -119,6 +121,7 @@ class MainViewModelTest : BaseViewModelTest() {
@BeforeEach @BeforeEach
fun setup() { fun setup() {
mockkStatic( mockkStatic(
Intent::getTotpDataOrNull,
Intent::getPasswordlessRequestDataIntentOrNull, Intent::getPasswordlessRequestDataIntentOrNull,
Intent::getAutofillSaveItemOrNull, Intent::getAutofillSaveItemOrNull,
Intent::getAutofillSelectionDataOrNull, Intent::getAutofillSelectionDataOrNull,
@ -134,6 +137,7 @@ class MainViewModelTest : BaseViewModelTest() {
@AfterEach @AfterEach
fun tearDown() { fun tearDown() {
unmockkStatic( unmockkStatic(
Intent::getTotpDataOrNull,
Intent::getPasswordlessRequestDataIntentOrNull, Intent::getPasswordlessRequestDataIntentOrNull,
Intent::getAutofillSaveItemOrNull, Intent::getAutofillSaveItemOrNull,
Intent::getAutofillSelectionDataOrNull, 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<Intent>()
val totpData = mockk<TotpData>()
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") @Suppress("MaxLineLength")
@Test @Test
fun `on ReceiveFirstIntent with share data should set the special circumstance to ShareNewSend`() { fun `on ReceiveFirstIntent with share data should set the special circumstance to ShareNewSend`() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent>() val mockIntent = mockk<Intent>()
val shareData = mockk<IntentManager.ShareData>() val shareData = mockk<IntentManager.ShareData>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null
@ -328,6 +355,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent>() val mockIntent = mockk<Intent>()
val autofillSelectionData = mockk<AutofillSelectionData>() val autofillSelectionData = mockk<AutofillSelectionData>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
@ -359,6 +387,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns "token" every { verificationToken } returns "token"
} }
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
@ -394,6 +423,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns "token" every { verificationToken } returns "token"
} }
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
@ -431,6 +461,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns token every { verificationToken } returns token
} }
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
@ -470,6 +501,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns token every { verificationToken } returns token
} }
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
@ -511,6 +543,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns token every { verificationToken } returns token
} }
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
@ -548,6 +581,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent>() val mockIntent = mockk<Intent>()
val autofillSaveItem = mockk<AutofillSaveItem>() val autofillSaveItem = mockk<AutofillSaveItem>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem
every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null
@ -578,6 +612,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { every {
mockIntent.getPasswordlessRequestDataIntentOrNull() mockIntent.getPasswordlessRequestDataIntentOrNull()
} returns passwordlessRequestData } returns passwordlessRequestData
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
@ -663,6 +698,7 @@ class MainViewModelTest : BaseViewModelTest() {
origin = "mockOrigin", origin = "mockOrigin",
) )
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
@ -701,6 +737,7 @@ class MainViewModelTest : BaseViewModelTest() {
) )
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
@ -773,6 +810,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent>() val mockIntent = mockk<Intent>()
val shareData = mockk<IntentManager.ShareData>() val shareData = mockk<IntentManager.ShareData>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } 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<Intent>()
val totpData = mockk<TotpData>()
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") @Suppress("MaxLineLength")
@Test @Test
fun `on ReceiveNewIntent with autofill data should set the special circumstance to AutofillSelection`() { fun `on ReceiveNewIntent with autofill data should set the special circumstance to AutofillSelection`() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent>() val mockIntent = mockk<Intent>()
val autofillSelectionData = mockk<AutofillSelectionData>() val autofillSelectionData = mockk<AutofillSelectionData>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
@ -829,6 +890,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent>() val mockIntent = mockk<Intent>()
val autofillSaveItem = mockk<AutofillSaveItem>() val autofillSaveItem = mockk<AutofillSaveItem>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem
every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null
@ -859,6 +921,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { every {
mockIntent.getPasswordlessRequestDataIntentOrNull() mockIntent.getPasswordlessRequestDataIntentOrNull()
} returns passwordlessRequestData } returns passwordlessRequestData
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } 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`() { fun `on ReceiveNewIntent with a Vault deeplink data should set the special circumstance to VaultShortcut`() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
every { getAutofillSelectionDataOrNull() } 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`() { fun `on ReceiveNewIntent with a password generator deeplink data should set the special circumstance to GeneratorShortcut`() {
val viewModel = createViewModel() val viewModel = createViewModel()
val mockIntent = mockk<Intent> { val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
@ -1043,6 +1108,7 @@ private fun createMockFido2RegistrationIntent(
fido2CredentialRequest: Fido2CredentialRequest = createMockFido2CredentialRequest(number = 1), fido2CredentialRequest: Fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
): Intent = mockk<Intent> { ): Intent = mockk<Intent> {
every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
@ -1056,6 +1122,7 @@ private fun createMockFido2AssertionIntent(
createMockFido2CredentialAssertionRequest(number = 1), createMockFido2CredentialAssertionRequest(number = 1),
): Intent = mockk<Intent> { ): Intent = mockk<Intent> {
every { getFido2AssertionRequestOrNull() } returns fido2CredentialAssertionRequest every { getFido2AssertionRequestOrNull() } returns fido2CredentialAssertionRequest
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null
@ -1070,6 +1137,7 @@ private fun createMockFido2GetCredentialsIntent(
), ),
): Intent = mockk<Intent> { ): Intent = mockk<Intent> {
every { getFido2GetCredentialsRequestOrNull() } returns fido2GetCredentialsRequest every { getFido2GetCredentialsRequestOrNull() } returns fido2GetCredentialsRequest
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null every { getAutofillSaveItemOrNull() } returns null

View file

@ -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.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.vault.model.TotpData
import io.mockk.mockk import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
@ -38,6 +39,7 @@ class SpecialCircumstanceExtensionsTest {
data = mockk(), data = mockk(),
shouldFinishWhenComplete = true, shouldFinishWhenComplete = true,
), ),
mockk<SpecialCircumstance.AddTotpLoginItem>(),
SpecialCircumstance.PasswordlessRequest( SpecialCircumstance.PasswordlessRequest(
passwordlessRequestData = mockk(), passwordlessRequestData = mockk(),
shouldFinishWhenComplete = true, shouldFinishWhenComplete = true,
@ -87,6 +89,7 @@ class SpecialCircumstanceExtensionsTest {
data = mockk(), data = mockk(),
shouldFinishWhenComplete = true, shouldFinishWhenComplete = true,
), ),
mockk<SpecialCircumstance.AddTotpLoginItem>(),
SpecialCircumstance.PasswordlessRequest( SpecialCircumstance.PasswordlessRequest(
passwordlessRequestData = mockk(), passwordlessRequestData = mockk(),
shouldFinishWhenComplete = true, shouldFinishWhenComplete = true,
@ -118,6 +121,7 @@ class SpecialCircumstanceExtensionsTest {
SpecialCircumstance.AutofillSave( SpecialCircumstance.AutofillSave(
autofillSaveItem = mockk(), autofillSaveItem = mockk(),
), ),
mockk<SpecialCircumstance.AddTotpLoginItem>(),
SpecialCircumstance.ShareNewSend( SpecialCircumstance.ShareNewSend(
data = mockk(), data = mockk(),
shouldFinishWhenComplete = true, shouldFinishWhenComplete = true,
@ -188,6 +192,7 @@ class SpecialCircumstanceExtensionsTest {
data = mockk(), data = mockk(),
shouldFinishWhenComplete = true, shouldFinishWhenComplete = true,
), ),
mockk<SpecialCircumstance.AddTotpLoginItem>(),
SpecialCircumstance.PasswordlessRequest( SpecialCircumstance.PasswordlessRequest(
passwordlessRequestData = mockk(), passwordlessRequestData = mockk(),
shouldFinishWhenComplete = true, shouldFinishWhenComplete = true,
@ -234,6 +239,7 @@ class SpecialCircumstanceExtensionsTest {
data = mockk(), data = mockk(),
shouldFinishWhenComplete = true, shouldFinishWhenComplete = true,
), ),
mockk<SpecialCircumstance.AddTotpLoginItem>(),
SpecialCircumstance.PasswordlessRequest( SpecialCircumstance.PasswordlessRequest(
passwordlessRequestData = mockk(), passwordlessRequestData = mockk(),
shouldFinishWhenComplete = true, shouldFinishWhenComplete = true,
@ -251,4 +257,31 @@ class SpecialCircumstanceExtensionsTest {
assertNull(specialCircumstance.toFido2GetCredentialsRequestOrNull()) assertNull(specialCircumstance.toFido2GetCredentialsRequestOrNull())
} }
} }
@Test
fun `toTotpDataOrNull should return a non-null value for AddTotpLoginItem`() {
val totpData = mockk<TotpData>()
assertEquals(
totpData,
SpecialCircumstance.AddTotpLoginItem(data = totpData).toTotpDataOrNull(),
)
}
@Test
fun `toTotpDataOrNull should return a null value for other types`() {
listOf(
mockk<SpecialCircumstance.AutofillSelection>(),
mockk<SpecialCircumstance.AutofillSave>(),
mockk<SpecialCircumstance.ShareNewSend>(),
mockk<SpecialCircumstance.PasswordlessRequest>(),
mockk<SpecialCircumstance.Fido2Save>(),
mockk<SpecialCircumstance.Fido2Assertion>(),
mockk<SpecialCircumstance.RegistrationEvent>(),
SpecialCircumstance.GeneratorShortcut,
SpecialCircumstance.VaultShortcut,
)
.forEach { specialCircumstance ->
assertNull(specialCircumstance.toTotpDataOrNull())
}
}
} }

View file

@ -117,7 +117,7 @@ class FakeNavHostController : NavHostController(context = mockk()) {
* Asserts the [currentRoute] matches the given [route]. * Asserts the [currentRoute] matches the given [route].
*/ */
fun assertCurrentRoute(route: String) { fun assertCurrentRoute(route: String) {
assertEquals(currentRoute, route) assertEquals(route, currentRoute)
} }
/** /**
@ -128,16 +128,16 @@ class FakeNavHostController : NavHostController(context = mockk()) {
navOptions: NavOptions? = null, navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null, navigatorExtras: Navigator.Extras? = null,
) { ) {
assertEquals(currentRoute, route) assertEquals(route, currentRoute)
assertEquals(lastNavigation?.navOptions, navOptions) assertEquals(navOptions, lastNavigation?.navOptions)
assertEquals(lastNavigation?.navigatorExtras, navigatorExtras) assertEquals(navigatorExtras, lastNavigation?.navigatorExtras)
} }
/** /**
* Asserts the [lastNavigation] includes the given [navOptions]. * Asserts the [lastNavigation] includes the given [navOptions].
*/ */
fun assertLastNavOptions(navOptions: NavOptions?) { fun assertLastNavOptions(navOptions: NavOptions?) {
assertEquals(lastNavigation?.navOptions, navOptions) assertEquals(navOptions, lastNavigation?.navOptions)
} }
data class Navigation( data class Navigation(

View file

@ -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: // Make sure navigating to vault unlocked for new sends works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForNewSend rootNavStateFlow.value = RootNavState.VaultUnlockedForNewSend
composeTestRule.runOnIdle { composeTestRule.runOnIdle {

View file

@ -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") @Suppress("MaxLineLength")
@Test @Test
fun `when the active user has an unlocked vault but the is a ShareNewSend special circumstance the nav state should be VaultUnlockedForNewSend`() { fun `when the active user has an unlocked vault but the is a ShareNewSend special circumstance the nav state should be VaultUnlockedForNewSend`() {