mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 11:59:57 +03:00
[PM-6702] 3# Open app from App Link to CompleteRegistration (#3619)
This commit is contained in:
parent
524b9e9a08
commit
e2cd3867dd
11 changed files with 429 additions and 3 deletions
|
@ -55,6 +55,17 @@
|
|||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="bitwarden.com" />
|
||||
<data android:host="bitwarden.pw" />
|
||||
<data android:host="bitwarden.eu" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_CREATE_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.fido2.ACTION_GET_PASSKEY" />
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.util.getFido2AssertionRequestOrNull
|
||||
|
@ -34,6 +35,7 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
|
@ -53,6 +55,7 @@ class MainViewModel @Inject constructor(
|
|||
private val vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val clock: Clock,
|
||||
) : BaseViewModel<MainState, MainEvent, MainAction>(
|
||||
initialState = MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
|
@ -188,6 +191,7 @@ class MainViewModel @Inject constructor(
|
|||
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
|
||||
val hasVaultShortcut = intent.isMyVaultShortcut
|
||||
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
|
||||
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
|
||||
val fido2CredentialAssertionRequest = intent.getFido2AssertionRequestOrNull()
|
||||
val fido2GetCredentialsRequest = intent.getFido2GetCredentialsRequestOrNull()
|
||||
when {
|
||||
|
@ -201,6 +205,17 @@ class MainViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
completeRegistrationData != null -> {
|
||||
if (authRepository.activeUserId != null) {
|
||||
authRepository.hasPendingAccountAddition = true
|
||||
}
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CompleteRegistration(
|
||||
completeRegistrationData = completeRegistrationData,
|
||||
timestamp = clock.millis(),
|
||||
)
|
||||
}
|
||||
|
||||
autofillSaveItem != null -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AutofillSave(
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package com.x8bit.bitwarden.data.auth.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
|
||||
/**
|
||||
* Checks if the given [Intent] contains data to complete registration.
|
||||
* The [CompleteRegistrationData] will be returned when present.
|
||||
*/
|
||||
fun Intent.getCompleteRegistrationDataIntentOrNull(): CompleteRegistrationData? {
|
||||
val sanitizedUriString = data.toString().replace("/#/", "/")
|
||||
val uri = runCatching { Uri.parse(sanitizedUriString) }.getOrNull() ?: return null
|
||||
uri.host ?: return null
|
||||
if (uri.path != "/finish-signup") return null
|
||||
val email = uri.getQueryParameter("email") ?: return null
|
||||
val verificationToken = uri.getQueryParameter("token") ?: return null
|
||||
val fromEmail = uri.getBooleanQueryParameter("fromEmail", true)
|
||||
return CompleteRegistrationData(
|
||||
email = email,
|
||||
verificationToken = verificationToken,
|
||||
fromEmail = fromEmail,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Required data to complete ongoing registration process.
|
||||
*
|
||||
* @property email The email of the user creating the account.
|
||||
* @property verificationToken The token required to finish the registration process.
|
||||
* @property fromEmail indicates that this information came from an email AppLink.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CompleteRegistrationData(
|
||||
val email: String,
|
||||
val verificationToken: String,
|
||||
val fromEmail: Boolean,
|
||||
) : Parcelable
|
|
@ -50,6 +50,15 @@ sealed class SpecialCircumstance : Parcelable {
|
|||
val shouldFinishWhenComplete: Boolean,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via AppLink in order to allow the user complete an ongoing registration.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CompleteRegistration(
|
||||
val completeRegistrationData: CompleteRegistrationData,
|
||||
val timestamp: Long,
|
||||
) : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via the credential manager framework in order to allow the user to
|
||||
* manually save a passkey to their vault.
|
||||
|
|
|
@ -19,6 +19,7 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? =
|
|||
SpecialCircumstance.GeneratorShortcut -> null
|
||||
SpecialCircumstance.VaultShortcut -> null
|
||||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.CompleteRegistration -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
}
|
||||
|
@ -35,6 +36,7 @@ fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData?
|
|||
SpecialCircumstance.GeneratorShortcut -> null
|
||||
SpecialCircumstance.VaultShortcut -> null
|
||||
is SpecialCircumstance.Fido2Save -> null
|
||||
is SpecialCircumstance.CompleteRegistration -> null
|
||||
is SpecialCircumstance.Fido2Assertion -> null
|
||||
is SpecialCircumstance.Fido2GetCredentials -> null
|
||||
}
|
||||
|
|
|
@ -85,8 +85,10 @@ fun RootNavScreen(
|
|||
}
|
||||
|
||||
val targetRoute = when (state) {
|
||||
RootNavState.Auth -> AUTH_GRAPH_ROUTE
|
||||
RootNavState.AuthWithWelcome -> AUTH_GRAPH_ROUTE
|
||||
RootNavState.Auth,
|
||||
is RootNavState.CompleteOngoingRegistration,
|
||||
RootNavState.AuthWithWelcome,
|
||||
-> AUTH_GRAPH_ROUTE
|
||||
RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE
|
||||
RootNavState.SetPassword -> SET_PASSWORD_ROUTE
|
||||
RootNavState.Splash -> SPLASH_ROUTE
|
||||
|
@ -189,6 +191,11 @@ fun RootNavScreen(
|
|||
navOptions = rootNavOptions,
|
||||
)
|
||||
}
|
||||
|
||||
is RootNavState.CompleteOngoingRegistration -> {
|
||||
navController.navigateToAuthGraph(rootNavOptions)
|
||||
// TODO PR-3622: add navigation to complete registration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ class RootNavViewModel @Inject constructor(
|
|||
action: RootNavAction.Internal.UserStateUpdateReceive,
|
||||
) {
|
||||
val userState = action.userState
|
||||
val specialCircumstance = action.specialCircumstance
|
||||
val updatedRootNavState = when {
|
||||
userState?.activeAccount?.trustedDevice?.isDeviceTrusted == false &&
|
||||
!userState.activeAccount.isVaultUnlocked &&
|
||||
|
@ -66,6 +67,15 @@ class RootNavViewModel @Inject constructor(
|
|||
|
||||
userState?.activeAccount?.needsPasswordReset == true -> RootNavState.ResetPassword
|
||||
|
||||
specialCircumstance is SpecialCircumstance.CompleteRegistration -> {
|
||||
RootNavState.CompleteOngoingRegistration(
|
||||
email = specialCircumstance.completeRegistrationData.email,
|
||||
verificationToken = specialCircumstance.completeRegistrationData.verificationToken,
|
||||
fromEmail = specialCircumstance.completeRegistrationData.fromEmail,
|
||||
timestamp = specialCircumstance.timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
userState == null ||
|
||||
!userState.activeAccount.isLoggedIn ||
|
||||
userState.hasPendingAccountAddition -> {
|
||||
|
@ -77,7 +87,7 @@ class RootNavViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
userState.activeAccount.isVaultUnlocked -> {
|
||||
when (val specialCircumstance = action.specialCircumstance) {
|
||||
when (specialCircumstance) {
|
||||
is SpecialCircumstance.AutofillSave -> {
|
||||
RootNavState.VaultUnlockedForAutofillSave(
|
||||
autofillSaveItem = specialCircumstance.autofillSaveItem,
|
||||
|
@ -122,6 +132,12 @@ class RootNavViewModel @Inject constructor(
|
|||
SpecialCircumstance.VaultShortcut,
|
||||
null,
|
||||
-> RootNavState.VaultUnlocked(activeUserId = userState.activeAccount.userId)
|
||||
|
||||
is SpecialCircumstance.CompleteRegistration -> {
|
||||
throw IllegalStateException(
|
||||
"Special circumstance should have been already handled.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,6 +258,17 @@ sealed class RootNavState : Parcelable {
|
|||
@Parcelize
|
||||
data object VaultUnlockedForNewSend : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show the screen to complete an ongoing registration process.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CompleteOngoingRegistration(
|
||||
val email: String,
|
||||
val verificationToken: String,
|
||||
val fromEmail: Boolean,
|
||||
val timestamp: Long,
|
||||
) : RootNavState()
|
||||
|
||||
/**
|
||||
* App should show the auth confirmation screen for an unlocked user.
|
||||
*/
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.bitwarden.vault.CipherView
|
|||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.util.getCompleteRegistrationDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.auth.util.getPasswordlessRequestDataIntentOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.manager.Fido2CredentialManager
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
|
@ -28,6 +29,7 @@ import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
|||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
|
@ -55,7 +57,11 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
|||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class MainViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val autofillSelectionManager: AutofillSelectionManager = AutofillSelectionManagerImpl()
|
||||
|
@ -96,6 +102,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
Intent::getPasswordlessRequestDataIntentOrNull,
|
||||
Intent::getAutofillSaveItemOrNull,
|
||||
Intent::getAutofillSelectionDataOrNull,
|
||||
Intent::getCompleteRegistrationDataIntentOrNull,
|
||||
Intent::getFido2CredentialRequestOrNull,
|
||||
)
|
||||
mockkStatic(
|
||||
|
@ -110,6 +117,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
Intent::getPasswordlessRequestDataIntentOrNull,
|
||||
Intent::getAutofillSaveItemOrNull,
|
||||
Intent::getAutofillSelectionDataOrNull,
|
||||
Intent::getCompleteRegistrationDataIntentOrNull,
|
||||
)
|
||||
unmockkStatic(
|
||||
Intent::isMyVaultShortcut,
|
||||
|
@ -261,6 +269,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
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 shareData
|
||||
every { mockIntent.isMyVaultShortcut } returns false
|
||||
every { mockIntent.isPasswordGeneratorShortcut } returns false
|
||||
|
@ -287,6 +296,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
val autofillSelectionData = mockk<AutofillSelectionData>()
|
||||
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { mockIntent.getAutofillSaveItemOrNull() } returns null
|
||||
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
|
||||
every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData
|
||||
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
|
||||
every { mockIntent.isMyVaultShortcut } returns false
|
||||
|
@ -306,6 +316,36 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with complete registration data should set the special circumstance to CompleteRegistration`() {
|
||||
val viewModel = createViewModel()
|
||||
val completeRegistrationData = mockk<CompleteRegistrationData>()
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
|
||||
every { authRepository.activeUserId } returns null
|
||||
|
||||
viewModel.trySendAction(
|
||||
MainAction.ReceiveFirstIntent(
|
||||
intent = mockIntent,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
SpecialCircumstance.CompleteRegistration(
|
||||
completeRegistrationData = completeRegistrationData,
|
||||
timestamp = FIXED_CLOCK.millis(),
|
||||
),
|
||||
specialCircumstanceManager.specialCircumstance,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on ReceiveFirstIntent with an autofill save item should set the special circumstance to AutofillSave`() {
|
||||
|
@ -315,6 +355,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem
|
||||
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
|
||||
|
@ -343,6 +384,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
} returns passwordlessRequestData
|
||||
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
|
||||
|
@ -426,6 +468,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
|
@ -459,6 +502,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
|
@ -527,6 +571,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
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 shareData
|
||||
every { mockIntent.isMyVaultShortcut } returns false
|
||||
every { mockIntent.isPasswordGeneratorShortcut } returns false
|
||||
|
@ -553,6 +598,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
val autofillSelectionData = mockk<AutofillSelectionData>()
|
||||
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { mockIntent.getAutofillSaveItemOrNull() } returns null
|
||||
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
|
||||
every { mockIntent.getAutofillSelectionDataOrNull() } returns autofillSelectionData
|
||||
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
|
||||
every { mockIntent.isMyVaultShortcut } returns false
|
||||
|
@ -581,6 +627,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem
|
||||
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
|
||||
|
@ -609,6 +656,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
} returns passwordlessRequestData
|
||||
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
|
||||
|
@ -635,6 +683,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns true
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
|
@ -659,6 +708,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns true
|
||||
}
|
||||
|
@ -700,6 +750,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
|||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
authRepository = authRepository,
|
||||
clock = FIXED_CLOCK,
|
||||
savedStateHandle = savedStateHandle.apply {
|
||||
set(SPECIAL_CIRCUMSTANCE_KEY, initialSpecialCircumstance)
|
||||
},
|
||||
|
@ -740,6 +791,7 @@ private fun createMockFido2RegistrationIntent(
|
|||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
|
@ -752,6 +804,7 @@ private fun createMockFido2AssertionIntent(
|
|||
every { getPasswordlessRequestDataIntentOrNull() } returns null
|
||||
every { getAutofillSelectionDataOrNull() } returns null
|
||||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
|
@ -767,6 +820,12 @@ private fun createMockFido2GetCredentialsIntent(
|
|||
every { getAutofillSaveItemOrNull() } returns null
|
||||
every { getFido2CredentialRequestOrNull() } returns null
|
||||
every { getFido2AssertionRequestOrNull() } returns null
|
||||
every { getCompleteRegistrationDataIntentOrNull() } returns null
|
||||
every { isMyVaultShortcut } returns false
|
||||
every { isPasswordGeneratorShortcut } returns false
|
||||
}
|
||||
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
package com.x8bit.bitwarden.data.auth.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CompleteRegistrationDataUtilsTest {
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCompleteRegistrationDataIntentOrNull valid URI returns CompleteRegistrationData`() {
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { data } returns mockk()
|
||||
}
|
||||
val uriMock = mockk<Uri>()
|
||||
every { Uri.parse(any()) } returns uriMock
|
||||
every { uriMock.host } returns "www.bitwarden.com"
|
||||
every { uriMock.path } returns "/finish-signup"
|
||||
every { uriMock.getQueryParameter("email") } returns "example@email.com"
|
||||
every { uriMock.getQueryParameter("token") } returns "verificationtoken"
|
||||
every { uriMock.getBooleanQueryParameter("fromEmail", true) } returns true
|
||||
|
||||
assertEquals(
|
||||
CompleteRegistrationData(
|
||||
email = "example@email.com",
|
||||
verificationToken = "verificationtoken",
|
||||
fromEmail = true,
|
||||
),
|
||||
mockIntent.getCompleteRegistrationDataIntentOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCompleteRegistrationDataIntentOrNull null data returns null`() {
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { data } returns mockk()
|
||||
}
|
||||
every { Uri.parse(any()) } returns null
|
||||
|
||||
assertNull(
|
||||
mockIntent.getCompleteRegistrationDataIntentOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCompleteRegistrationDataIntentOrNull Uri with no host`() {
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { data } returns mockk()
|
||||
}
|
||||
val uriMock = mockk<Uri>()
|
||||
every { Uri.parse(any()) } returns null
|
||||
every { uriMock.host } returns null
|
||||
|
||||
assertNull(
|
||||
mockIntent.getCompleteRegistrationDataIntentOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCompleteRegistrationDataIntentOrNull URI does not contain finish-signup path`() {
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { data } returns mockk()
|
||||
}
|
||||
val uriMock = mockk<Uri>()
|
||||
every { Uri.parse(any()) } returns null
|
||||
every { uriMock.host } returns null
|
||||
every { uriMock.path } returns "/finish"
|
||||
assertNull(
|
||||
mockIntent.getCompleteRegistrationDataIntentOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCompleteRegistrationDataIntentOrNull URI does not contain parameter email`() {
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { data } returns mockk()
|
||||
}
|
||||
val uriMock = mockk<Uri>()
|
||||
every { Uri.parse(any()) } returns uriMock
|
||||
every { uriMock.host } returns "www.bitwarden.com"
|
||||
every { uriMock.path } returns "/finish-signup"
|
||||
every { uriMock.getQueryParameter("email") } returns null
|
||||
assertNull(
|
||||
mockIntent.getCompleteRegistrationDataIntentOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCompleteRegistrationDataIntentOrNull URI does not contain parameter token`() {
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { data } returns mockk()
|
||||
}
|
||||
val uriMock = mockk<Uri>()
|
||||
every { Uri.parse(any()) } returns uriMock
|
||||
every { uriMock.host } returns "www.bitwarden.com"
|
||||
every { uriMock.path } returns "/finish-signup"
|
||||
every { uriMock.getQueryParameter("email") } returns "example@email.com"
|
||||
every { uriMock.getQueryParameter("token") } returns null
|
||||
assertNull(
|
||||
mockIntent.getCompleteRegistrationDataIntentOrNull(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -9,6 +9,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.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
|
@ -17,7 +18,11 @@ import io.mockk.mockk
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class RootNavViewModelTest : BaseViewModelTest() {
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
|
@ -550,6 +555,129 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when there are no accounts but there is a CompleteRegistration special circumstance the nav state should be CompleteRegistration`() {
|
||||
every { authRepository.hasPendingAccountAddition } returns false
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CompleteRegistration(
|
||||
CompleteRegistrationData(
|
||||
email = "example@email.com",
|
||||
verificationToken = "token",
|
||||
fromEmail = true,
|
||||
),
|
||||
FIXED_CLOCK.instant().toEpochMilli(),
|
||||
)
|
||||
mutableUserStateFlow.tryEmit(null)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.CompleteOngoingRegistration(
|
||||
email = "example@email.com",
|
||||
verificationToken = "token",
|
||||
fromEmail = true,
|
||||
timestamp = FIXED_CLOCK.instant().toEpochMilli(),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is a CompleteRegistration special circumstance the nav state should be CompleteRegistration`() {
|
||||
every { authRepository.hasPendingAccountAddition } returns true
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CompleteRegistration(
|
||||
CompleteRegistrationData(
|
||||
email = "example@email.com",
|
||||
verificationToken = "token",
|
||||
fromEmail = true,
|
||||
),
|
||||
FIXED_CLOCK.instant().toEpochMilli(),
|
||||
)
|
||||
mutableUserStateFlow.tryEmit(
|
||||
UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "name",
|
||||
email = "email",
|
||||
avatarColorHex = "avatarHexColor",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.CompleteOngoingRegistration(
|
||||
email = "example@email.com",
|
||||
verificationToken = "token",
|
||||
fromEmail = true,
|
||||
timestamp = FIXED_CLOCK.instant().toEpochMilli(),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when the active user has a locked vault but there is a CompleteRegistration special circumstance the nav state should be CompleteRegistration`() {
|
||||
every { authRepository.hasPendingAccountAddition } returns true
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.CompleteRegistration(
|
||||
CompleteRegistrationData(
|
||||
email = "example@email.com",
|
||||
verificationToken = "token",
|
||||
fromEmail = true,
|
||||
),
|
||||
FIXED_CLOCK.instant().toEpochMilli(),
|
||||
)
|
||||
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 = false,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
RootNavState.CompleteOngoingRegistration(
|
||||
email = "example@email.com",
|
||||
verificationToken = "token",
|
||||
fromEmail = true,
|
||||
timestamp = FIXED_CLOCK.instant().toEpochMilli(),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when the active user has a locked vault the nav state should be VaultLocked`() {
|
||||
mutableUserStateFlow.tryEmit(
|
||||
|
@ -583,4 +711,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
|||
authRepository = authRepository,
|
||||
specialCircumstanceManager = specialCircumstanceManager,
|
||||
)
|
||||
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue