mirror of
https://github.com/bitwarden/android.git
synced 2024-11-26 19:36:18 +03:00
BIT-809: Generate fingerprint on Login with Device (#781)
This commit is contained in:
parent
cd020f2af9
commit
3635d368f9
18 changed files with 396 additions and 39 deletions
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
|
@ -10,6 +11,21 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
|||
* Source of authentication information and functionality from the Bitwarden SDK.
|
||||
*/
|
||||
interface AuthSdkSource {
|
||||
/**
|
||||
* Gets the data needed to create a new auth request.
|
||||
*/
|
||||
suspend fun getNewAuthRequest(
|
||||
email: String,
|
||||
): Result<AuthRequestResponse>
|
||||
|
||||
/**
|
||||
* Gets the fingerprint phrase for this [email] and [publicKey].
|
||||
*/
|
||||
suspend fun getUserFingerprint(
|
||||
email: String,
|
||||
publicKey: String,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Creates a hashed password provided the given [email], [password], [kdf], and [purpose].
|
||||
*/
|
||||
|
@ -21,7 +37,7 @@ interface AuthSdkSource {
|
|||
): Result<String>
|
||||
|
||||
/**
|
||||
* Creates a set of encryption key information for registraation pers
|
||||
* Creates a set of encryption key information for registration.
|
||||
*/
|
||||
suspend fun makeRegisterKeys(
|
||||
email: String,
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.FingerprintRequest
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.sdk.ClientAuth
|
||||
import com.bitwarden.sdk.ClientPlatform
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
|
||||
|
@ -15,8 +18,29 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
|
|||
*/
|
||||
class AuthSdkSourceImpl(
|
||||
private val clientAuth: ClientAuth,
|
||||
private val clientPlatform: ClientPlatform,
|
||||
) : AuthSdkSource {
|
||||
|
||||
override suspend fun getNewAuthRequest(
|
||||
email: String,
|
||||
): Result<AuthRequestResponse> = runCatching {
|
||||
clientAuth.newAuthRequest(
|
||||
email = email,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getUserFingerprint(
|
||||
email: String,
|
||||
publicKey: String,
|
||||
): Result<String> = runCatching {
|
||||
clientPlatform.fingerprint(
|
||||
req = FingerprintRequest(
|
||||
fingerprintMaterial = email,
|
||||
publicKey = publicKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun hashPassword(
|
||||
email: String,
|
||||
password: String,
|
||||
|
|
|
@ -20,5 +20,8 @@ object AuthSdkModule {
|
|||
@Singleton
|
||||
fun provideAuthSdkSource(
|
||||
client: Client,
|
||||
): AuthSdkSource = AuthSdkSourceImpl(clientAuth = client.auth())
|
||||
): AuthSdkSource = AuthSdkSourceImpl(
|
||||
clientAuth = client.auth(),
|
||||
clientPlatform = client.platform(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
|
@ -136,6 +137,11 @@ interface AuthRepository : AuthenticatorProvider {
|
|||
*/
|
||||
suspend fun getAuthRequests(): AuthRequestsResult
|
||||
|
||||
/**
|
||||
* Gets a unique fingerprint phrase for this user.
|
||||
*/
|
||||
suspend fun getFingerprintPhrase(email: String): UserFingerprintResult
|
||||
|
||||
/**
|
||||
* Get a [Boolean] indicating whether this is a known device.
|
||||
*/
|
||||
|
|
|
@ -33,6 +33,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
|
@ -410,6 +411,7 @@ class AuthRepositoryImpl(
|
|||
is PasswordHintResponseJson.Error -> {
|
||||
PasswordHintResult.Error(it.errorMessage)
|
||||
}
|
||||
|
||||
PasswordHintResponseJson.Success -> PasswordHintResult.Success
|
||||
}
|
||||
},
|
||||
|
@ -466,6 +468,22 @@ class AuthRepositoryImpl(
|
|||
},
|
||||
)
|
||||
|
||||
override suspend fun getFingerprintPhrase(
|
||||
email: String,
|
||||
): UserFingerprintResult =
|
||||
authSdkSource.getNewAuthRequest(email)
|
||||
.flatMap { requestResponse ->
|
||||
authSdkSource
|
||||
.getUserFingerprint(
|
||||
email = email,
|
||||
publicKey = requestResponse.publicKey,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { UserFingerprintResult.Error },
|
||||
onSuccess = { UserFingerprintResult.Success(it) },
|
||||
)
|
||||
|
||||
override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult =
|
||||
devicesService
|
||||
.getIsKnownDevice(
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of getting the user fingerprint.
|
||||
*/
|
||||
sealed class UserFingerprintResult {
|
||||
/**
|
||||
* Contains the user fingerprint.
|
||||
*/
|
||||
data class Success(
|
||||
val fingerprint: String,
|
||||
) : UserFingerprintResult()
|
||||
|
||||
/**
|
||||
* There was an error getting the user fingerprint.
|
||||
*/
|
||||
data object Error : UserFingerprintResult()
|
||||
}
|
|
@ -69,7 +69,11 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
|||
)
|
||||
},
|
||||
onNavigateToEnterpriseSignOn = { navController.navigateToEnterpriseSignOn() },
|
||||
onNavigateToLoginWithDevice = { navController.navigateToLoginWithDevice() },
|
||||
onNavigateToLoginWithDevice = { emailAddress ->
|
||||
navController.navigateToLoginWithDevice(
|
||||
emailAddress = emailAddress,
|
||||
)
|
||||
},
|
||||
onNavigateToTwoFactorLogin = { navController.navigateToTwoFactorLogin() },
|
||||
)
|
||||
loginWithDeviceDestination(
|
||||
|
|
|
@ -45,7 +45,7 @@ fun NavGraphBuilder.loginDestination(
|
|||
onNavigateBack: () -> Unit,
|
||||
onNavigateToMasterPasswordHint: (emailAddress: String) -> Unit,
|
||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||
onNavigateToLoginWithDevice: () -> Unit,
|
||||
onNavigateToLoginWithDevice: (emailAddress: String) -> Unit,
|
||||
onNavigateToTwoFactorLogin: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
|
|
|
@ -61,12 +61,12 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
fun LoginScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToMasterPasswordHint: (String) -> Unit,
|
||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||
onNavigateToLoginWithDevice: () -> Unit,
|
||||
onNavigateToLoginWithDevice: (emailAddress: String) -> Unit,
|
||||
onNavigateToTwoFactorLogin: () -> Unit,
|
||||
viewModel: LoginViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
|
@ -85,7 +85,10 @@ fun LoginScreen(
|
|||
}
|
||||
|
||||
LoginEvent.NavigateToEnterpriseSignOn -> onNavigateToEnterpriseSignOn()
|
||||
LoginEvent.NavigateToLoginWithDevice -> onNavigateToLoginWithDevice()
|
||||
is LoginEvent.NavigateToLoginWithDevice -> {
|
||||
onNavigateToLoginWithDevice(event.emailAddress)
|
||||
}
|
||||
|
||||
LoginEvent.NavigateToTwoFactorLogin -> onNavigateToTwoFactorLogin()
|
||||
is LoginEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||
|
|
|
@ -217,7 +217,7 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleLoginWithDeviceButtonClicked() {
|
||||
sendEvent(LoginEvent.NavigateToLoginWithDevice)
|
||||
sendEvent(LoginEvent.NavigateToLoginWithDevice(state.emailAddress))
|
||||
}
|
||||
|
||||
private fun attemptLogin() {
|
||||
|
@ -310,7 +310,9 @@ sealed class LoginEvent {
|
|||
/**
|
||||
* Navigates to the login with device screen.
|
||||
*/
|
||||
data object NavigateToLoginWithDevice : LoginEvent()
|
||||
data class NavigateToLoginWithDevice(
|
||||
val emailAddress: String,
|
||||
) : LoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the two-factor login screen.
|
||||
|
|
|
@ -1,17 +1,34 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
private const val LOGIN_WITH_DEVICE_ROUTE = "login_with_device"
|
||||
private const val EMAIL_ADDRESS: String = "email_address"
|
||||
private const val LOGIN_WITH_DEVICE_PREFIX = "login_with_device"
|
||||
private const val LOGIN_WITH_DEVICE_ROUTE = "$LOGIN_WITH_DEVICE_PREFIX/{$EMAIL_ADDRESS}"
|
||||
|
||||
/**
|
||||
* Class to retrieve login with device arguments from the [SavedStateHandle].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class LoginWithDeviceArgs(val emailAddress: String) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the Login with Device screen.
|
||||
*/
|
||||
fun NavController.navigateToLoginWithDevice(navOptions: NavOptions? = null) {
|
||||
this.navigate(LOGIN_WITH_DEVICE_ROUTE, navOptions)
|
||||
fun NavController.navigateToLoginWithDevice(
|
||||
emailAddress: String,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate("$LOGIN_WITH_DEVICE_PREFIX/$emailAddress", navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,10 +2,16 @@ package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -16,19 +22,20 @@ private const val KEY_STATE = "state"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class LoginWithDeviceViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<LoginWithDeviceState, LoginWithDeviceEvent, LoginWithDeviceAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: LoginWithDeviceState(
|
||||
emailAddress = LoginWithDeviceArgs(savedStateHandle).emailAddress,
|
||||
viewState = LoginWithDeviceState.ViewState.Loading,
|
||||
),
|
||||
) {
|
||||
init {
|
||||
mutableStateFlow.update {
|
||||
// TODO BIT-809: Pull phrase from SDK
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate",
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
LoginWithDeviceAction.Internal.FingerprintPhraseReceived(
|
||||
result = authRepository.getFingerprintPhrase(state.emailAddress),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -39,6 +46,10 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
LoginWithDeviceAction.CloseButtonClick -> handleCloseButtonClicked()
|
||||
LoginWithDeviceAction.ResendNotificationClick -> handleResendNotificationClicked()
|
||||
LoginWithDeviceAction.ViewAllLogInOptionsClick -> handleViewAllLogInOptionsClicked()
|
||||
|
||||
is LoginWithDeviceAction.Internal.FingerprintPhraseReceived -> {
|
||||
handleFingerprintPhraseReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,6 +65,32 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
private fun handleViewAllLogInOptionsClicked() {
|
||||
sendEvent(LoginWithDeviceEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleFingerprintPhraseReceived(
|
||||
action: LoginWithDeviceAction.Internal.FingerprintPhraseReceived,
|
||||
) {
|
||||
when (action.result) {
|
||||
is UserFingerprintResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = action.result.fingerprint,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is UserFingerprintResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,6 +98,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
*/
|
||||
@Parcelize
|
||||
data class LoginWithDeviceState(
|
||||
val emailAddress: String,
|
||||
val viewState: ViewState,
|
||||
) : Parcelable {
|
||||
/**
|
||||
|
@ -133,4 +171,16 @@ sealed class LoginWithDeviceAction {
|
|||
* Indicates that the "View all log in options" text has been clicked.
|
||||
*/
|
||||
data object ViewAllLogInOptionsClick : LoginWithDeviceAction()
|
||||
|
||||
/**
|
||||
* Models actions for internal use by the view model.
|
||||
*/
|
||||
sealed class Internal : LoginWithDeviceAction() {
|
||||
/**
|
||||
* A fingerprint phrase for this user has been received.
|
||||
*/
|
||||
data class FingerprintPhraseReceived(
|
||||
val result: UserFingerprintResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.FingerprintRequest
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.sdk.ClientAuth
|
||||
import com.bitwarden.sdk.ClientPlatform
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import io.mockk.coEvery
|
||||
|
@ -17,11 +20,61 @@ import org.junit.jupiter.api.Test
|
|||
|
||||
class AuthSdkSourceTest {
|
||||
private val clientAuth = mockk<ClientAuth>()
|
||||
private val clientPlatform = mockk<ClientPlatform>()
|
||||
|
||||
private val authSkdSource: AuthSdkSource = AuthSdkSourceImpl(
|
||||
clientAuth = clientAuth,
|
||||
clientPlatform = clientPlatform,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `getNewAuthRequest should call SDK and return a Result with correct data`() = runBlocking {
|
||||
val email = "test@gmail.com"
|
||||
val expectedResult = mockk<AuthRequestResponse>()
|
||||
coEvery {
|
||||
clientAuth.newAuthRequest(email)
|
||||
} returns expectedResult
|
||||
|
||||
val result = authSkdSource.getNewAuthRequest(email)
|
||||
|
||||
assertEquals(
|
||||
expectedResult.asSuccess(),
|
||||
result,
|
||||
)
|
||||
coVerify {
|
||||
clientAuth.newAuthRequest(email)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserFingerprint should call SDK and return a Result with correct data`() = runBlocking {
|
||||
val email = "email@gmail.com"
|
||||
val publicKey = "publicKey"
|
||||
val expectedResult = "fingerprint"
|
||||
coEvery {
|
||||
clientPlatform.fingerprint(
|
||||
req = FingerprintRequest(
|
||||
fingerprintMaterial = email,
|
||||
publicKey = publicKey,
|
||||
),
|
||||
)
|
||||
} returns expectedResult
|
||||
|
||||
val result = authSkdSource.getUserFingerprint(email, publicKey)
|
||||
assertEquals(
|
||||
expectedResult.asSuccess(),
|
||||
result,
|
||||
)
|
||||
coVerify {
|
||||
clientPlatform.fingerprint(
|
||||
req = FingerprintRequest(
|
||||
fingerprintMaterial = email,
|
||||
publicKey = publicKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hashPassword should call SDK and return a Result with the correct data`() = runBlocking {
|
||||
val email = "email"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
|
@ -44,6 +45,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
|
@ -1374,6 +1376,68 @@ class AuthRepositoryTest {
|
|||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserFingerprint should return failure when source returns failure`() = runTest {
|
||||
coEvery {
|
||||
authSdkSource.getNewAuthRequest(EMAIL)
|
||||
} returns Result.success(
|
||||
mockk<AuthRequestResponse> {
|
||||
every { publicKey } returns PUBLIC_KEY
|
||||
},
|
||||
)
|
||||
coEvery {
|
||||
authSdkSource.getUserFingerprint(
|
||||
email = EMAIL,
|
||||
publicKey = PUBLIC_KEY,
|
||||
)
|
||||
} returns Result.failure(Throwable())
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
val result = repository.getFingerprintPhrase(EMAIL)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authSdkSource.getNewAuthRequest(EMAIL)
|
||||
authSdkSource.getUserFingerprint(
|
||||
email = EMAIL,
|
||||
publicKey = PUBLIC_KEY,
|
||||
)
|
||||
}
|
||||
assertEquals(UserFingerprintResult.Error, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserFingerprint should return success when source returns success`() = runTest {
|
||||
val fingerprint = "fingerprint"
|
||||
coEvery {
|
||||
authSdkSource.getNewAuthRequest(EMAIL)
|
||||
} returns Result.success(
|
||||
AuthRequestResponse(
|
||||
fingerprint = fingerprint,
|
||||
publicKey = PUBLIC_KEY,
|
||||
privateKey = "key",
|
||||
accessCode = "accessCode",
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
authSdkSource.getUserFingerprint(
|
||||
email = EMAIL,
|
||||
publicKey = PUBLIC_KEY,
|
||||
)
|
||||
} returns Result.success(fingerprint)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
val result = repository.getFingerprintPhrase(EMAIL)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authSdkSource.getNewAuthRequest(EMAIL)
|
||||
authSdkSource.getUserFingerprint(
|
||||
email = EMAIL,
|
||||
publicKey = PUBLIC_KEY,
|
||||
)
|
||||
}
|
||||
assertEquals(UserFingerprintResult.Success(fingerprint), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getIsKnownDevice should return failure when service returns failure`() = runTest {
|
||||
coEvery {
|
||||
|
|
|
@ -297,15 +297,16 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `NavigateToLoginWithDevice should call onNavigateToLoginWithDevice`() {
|
||||
mutableEventFlow.tryEmit(LoginEvent.NavigateToLoginWithDevice)
|
||||
mutableEventFlow.tryEmit(LoginEvent.NavigateToLoginWithDevice(EMAIL))
|
||||
assertTrue(onNavigateToLoginWithDeviceCalled)
|
||||
}
|
||||
}
|
||||
|
||||
private const val EMAIL = "active@bitwarden.com"
|
||||
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||
userId = "activeUserId",
|
||||
name = "Active User",
|
||||
email = "active@bitwarden.com",
|
||||
email = EMAIL,
|
||||
avatarColorHex = "#aa00aa",
|
||||
environmentLabel = "bitwarden.com",
|
||||
isActive = true,
|
||||
|
@ -315,7 +316,7 @@ private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
|||
|
||||
private val DEFAULT_STATE =
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
emailAddress = EMAIL,
|
||||
captchaToken = null,
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
|
|
|
@ -40,14 +40,14 @@ import org.junit.jupiter.api.Test
|
|||
class LoginViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val savedStateHandle = SavedStateHandle().also {
|
||||
it["email_address"] = "test@gmail.com"
|
||||
it["email_address"] = EMAIL
|
||||
}
|
||||
private val mutableCaptchaTokenResultFlow =
|
||||
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
|
||||
private val authRepository: AuthRepository = mockk(relaxed = true) {
|
||||
coEvery {
|
||||
getIsKnownDevice("test@gmail.com")
|
||||
getIsKnownDevice(EMAIL)
|
||||
} returns KnownDeviceResult.Success(false)
|
||||
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
|
@ -160,7 +160,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
shouldShowLoginWithDevice = true,
|
||||
)
|
||||
coEvery {
|
||||
authRepository.getIsKnownDevice("test@gmail.com")
|
||||
authRepository.getIsKnownDevice(EMAIL)
|
||||
} returns KnownDeviceResult.Success(true)
|
||||
val viewModel = createViewModel()
|
||||
|
||||
|
@ -172,7 +172,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `should have default state when isKnownDevice returns error`() = runTest {
|
||||
coEvery {
|
||||
authRepository.getIsKnownDevice("test@gmail.com")
|
||||
authRepository.getIsKnownDevice(EMAIL)
|
||||
} returns KnownDeviceResult.Error
|
||||
val viewModel = createViewModel()
|
||||
|
||||
|
@ -246,7 +246,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
fun `LoginButtonClick login returns error should update errorDialogState`() = runTest {
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = "test@gmail.com",
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
|
@ -275,7 +275,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -283,7 +283,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
fun `LoginButtonClick login returns success should update loadingDialogState`() = runTest {
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = "test@gmail.com",
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
|
@ -306,7 +306,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,7 +319,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
} returns mockkUri
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = "test@gmail.com",
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
|
@ -331,7 +331,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(LoginEvent.NavigateToCaptcha(uri = mockkUri), awaitItem())
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,7 +342,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(LoginAction.MasterPasswordHintClick)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
LoginEvent.NavigateToMasterPasswordHint("test@gmail.com"),
|
||||
LoginEvent.NavigateToMasterPasswordHint(EMAIL),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
@ -355,7 +355,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(LoginAction.LoginWithDeviceButtonClick)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
LoginEvent.NavigateToLoginWithDevice,
|
||||
LoginEvent.NavigateToLoginWithDevice(EMAIL),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
@ -435,7 +435,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
fun `captchaTokenFlow success update should trigger a login`() = runTest {
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = "test@gmail.com",
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = "token",
|
||||
)
|
||||
|
@ -443,7 +443,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
createViewModel()
|
||||
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token"))
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "", captchaToken = "token")
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = "token")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -456,8 +456,9 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@gmail.com"
|
||||
private val DEFAULT_STATE = LoginState(
|
||||
emailAddress = "test@gmail.com",
|
||||
emailAddress = EMAIL,
|
||||
passwordInput = "",
|
||||
isLoginButtonEnabled = false,
|
||||
environmentLabel = Environment.Us.label,
|
||||
|
|
|
@ -105,7 +105,9 @@ class LoginWithDeviceScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@gmail.com"
|
||||
private val DEFAULT_STATE = LoginWithDeviceState(
|
||||
emailAddress = EMAIL,
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate",
|
||||
),
|
||||
|
|
|
@ -2,14 +2,25 @@ package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val savedStateHandle = SavedStateHandle()
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
coEvery {
|
||||
getFingerprintPhrase(EMAIL)
|
||||
} returns UserFingerprintResult.Success("initialFingerprint")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
|
@ -17,6 +28,26 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
coVerify { authRepository.getFingerprintPhrase(EMAIL) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when set`() = runTest {
|
||||
val newEmail = "newEmail@gmail.com"
|
||||
coEvery {
|
||||
authRepository.getFingerprintPhrase(newEmail)
|
||||
} returns UserFingerprintResult.Success("initialFingerprint")
|
||||
val state = LoginWithDeviceState(
|
||||
emailAddress = newEmail,
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = "initialFingerprint",
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(state)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(state, awaitItem())
|
||||
}
|
||||
coVerify { authRepository.getFingerprintPhrase(newEmail) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -57,15 +88,59 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): LoginWithDeviceViewModel =
|
||||
@Test
|
||||
fun `on fingerprint result success received should show content`() = runTest {
|
||||
val newFingerprint = "newFingerprint"
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
viewModel.actionChannel.trySend(
|
||||
LoginWithDeviceAction.Internal.FingerprintPhraseReceived(
|
||||
result = UserFingerprintResult.Success(newFingerprint),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = newFingerprint,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on fingerprint result failure received should show error`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
viewModel.actionChannel.trySend(
|
||||
LoginWithDeviceAction.Internal.FingerprintPhraseReceived(
|
||||
result = UserFingerprintResult.Error,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: LoginWithDeviceState = DEFAULT_STATE,
|
||||
): LoginWithDeviceViewModel =
|
||||
LoginWithDeviceViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@gmail.com"
|
||||
private val DEFAULT_STATE = LoginWithDeviceState(
|
||||
emailAddress = EMAIL,
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate",
|
||||
fingerprintPhrase = "initialFingerprint",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue