mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
BIT-102: Create account functionality (#132)
This commit is contained in:
parent
6f212066e3
commit
79c953b605
35 changed files with 1134 additions and 114 deletions
|
@ -2,7 +2,7 @@ package com.x8bit.bitwarden
|
|||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
|
|
@ -2,6 +2,8 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
|
@ -12,4 +14,7 @@ interface AccountsApi {
|
|||
|
||||
@POST("/accounts/prelogin")
|
||||
suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson>
|
||||
|
||||
@POST("/accounts/register")
|
||||
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success>
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ object NetworkModule {
|
|||
@Singleton
|
||||
fun providesAccountService(
|
||||
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
|
||||
): AccountsService = AccountsServiceImpl(retrofit.create())
|
||||
json: Json,
|
||||
): AccountsService = AccountsServiceImpl(retrofit.create(), json)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson.Keys
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body for register.
|
||||
*
|
||||
* @param email the email to be registered.
|
||||
* @param masterPasswordHash the master password (encrypted).
|
||||
* @param masterPasswordHint the hint for the master password (nullable).
|
||||
* @param captchaResponse the captcha bypass token.
|
||||
* @param key the user key for the request (encrypted).
|
||||
* @param keys a [Keys] object containing public and private keys.
|
||||
* @param kdfType the kdf type represented as an [Int].
|
||||
* @param kdfIterations the number of kdf iterations.
|
||||
*/
|
||||
@Serializable
|
||||
data class RegisterRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
|
||||
@SerialName("masterPasswordHash")
|
||||
val masterPasswordHash: String,
|
||||
|
||||
@SerialName("masterPasswordHint")
|
||||
val masterPasswordHint: String?,
|
||||
|
||||
@SerialName("captchaResponse")
|
||||
val captchaResponse: String?,
|
||||
|
||||
@SerialName("key")
|
||||
val key: String,
|
||||
|
||||
@SerialName("keys")
|
||||
val keys: Keys,
|
||||
|
||||
@SerialName("kdf")
|
||||
val kdfType: KdfTypeJson,
|
||||
|
||||
@SerialName("kdfIterations")
|
||||
val kdfIterations: UInt,
|
||||
) {
|
||||
|
||||
/**
|
||||
* A keys object containing public and private keys.
|
||||
*
|
||||
* @param publicKey the public key (encrypted).
|
||||
* @param encryptedPrivateKey the private key (encrypted).
|
||||
*/
|
||||
@Serializable
|
||||
data class Keys(
|
||||
@SerialName("publicKey")
|
||||
val publicKey: String,
|
||||
|
||||
@SerialName("encryptedPrivateKey")
|
||||
val encryptedPrivateKey: String,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models response bodies for the register request.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class RegisterResponseJson {
|
||||
|
||||
/**
|
||||
* Models a successful json response of the register request.
|
||||
*
|
||||
* @param captchaBypassToken the bypass token.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
@SerialName("captchaBypassToken")
|
||||
val captchaBypassToken: String,
|
||||
) : RegisterResponseJson()
|
||||
|
||||
/**
|
||||
* Models a json body of a captcha error.
|
||||
*
|
||||
* @param validationErrors object containing error validations of the response.
|
||||
*/
|
||||
@Serializable
|
||||
data class CaptchaRequired(
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: ValidationErrors,
|
||||
) : RegisterResponseJson() {
|
||||
|
||||
/**
|
||||
* Error validations containing a HCaptcha Site Key.
|
||||
*
|
||||
* @param captchaKeys keys for attempting captcha verification.
|
||||
*/
|
||||
@Serializable
|
||||
data class ValidationErrors(
|
||||
@SerialName("HCaptcha_SiteKey")
|
||||
val captchaKeys: List<String>,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for querying accounts endpoints.
|
||||
|
@ -11,4 +13,9 @@ interface AccountsService {
|
|||
* Make pre login request to get KDF params.
|
||||
*/
|
||||
suspend fun preLogin(email: String): Result<PreLoginResponseJson>
|
||||
|
||||
/**
|
||||
* Register a new account to Bitwarden.
|
||||
*/
|
||||
suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson>
|
||||
}
|
||||
|
|
|
@ -3,11 +3,31 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
class AccountsServiceImpl constructor(
|
||||
private val accountsApi: AccountsApi,
|
||||
private val json: Json,
|
||||
) : AccountsService {
|
||||
|
||||
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
|
||||
accountsApi.preLogin(PreLoginRequestJson(email = email))
|
||||
|
||||
// TODO add error parsing and pass along error message for validations BIT-763
|
||||
override suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson> =
|
||||
accountsApi
|
||||
.register(body)
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
|
||||
code = HttpURLConnection.HTTP_BAD_REQUEST,
|
||||
json = json,
|
||||
) ?: throw throwable
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.sdk.util
|
||||
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.ARGON2_ID
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.PBKDF2_SHA256
|
||||
|
||||
/**
|
||||
* Convert a [Kdf] to a [KdfTypeJson].
|
||||
*/
|
||||
fun Kdf.toKdfTypeJson(): KdfTypeJson =
|
||||
when (this) {
|
||||
is Kdf.Argon2id -> ARGON2_ID
|
||||
is Kdf.Pbkdf2 -> PBKDF2_SHA256
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
|
@ -26,6 +27,12 @@ interface AuthRepository {
|
|||
*/
|
||||
var rememberedEmailAddress: String?
|
||||
|
||||
/**
|
||||
* The currently selected region label (`null` if not set).
|
||||
*/
|
||||
// TODO replace this with a more robust selected region object BIT-725
|
||||
var selectedRegionLabel: String
|
||||
|
||||
/**
|
||||
* Attempt to login with the given email and password. Updated access token will be reflected
|
||||
* in [authStateFlow].
|
||||
|
@ -41,6 +48,16 @@ interface AuthRepository {
|
|||
*/
|
||||
fun logout()
|
||||
|
||||
/**
|
||||
* Attempt to register a new account with the given parameters.
|
||||
*/
|
||||
suspend fun register(
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
captchaToken: String?,
|
||||
): RegisterResult
|
||||
|
||||
/**
|
||||
* Set the value of [captchaTokenResultFlow].
|
||||
*/
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.CaptchaRequired
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.Success
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
|
@ -23,6 +28,8 @@ import kotlinx.coroutines.flow.update
|
|||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val DEFAULT_KDF_ITERATIONS = 600000
|
||||
|
||||
/**
|
||||
* Default implementation of [AuthRepository].
|
||||
*/
|
||||
|
@ -49,6 +56,9 @@ class AuthRepositoryImpl @Inject constructor(
|
|||
authDiskSource.rememberedEmailAddress = value
|
||||
}
|
||||
|
||||
// TODO Handle selected region functionality BIT-725
|
||||
override var selectedRegionLabel: String = "bitwarden.us"
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
|
@ -93,6 +103,56 @@ class AuthRepositoryImpl @Inject constructor(
|
|||
mutableAuthStateFlow.update { AuthState.Unauthenticated }
|
||||
}
|
||||
|
||||
override suspend fun register(
|
||||
email: String,
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
captchaToken: String?,
|
||||
): RegisterResult {
|
||||
val kdf = Kdf.Pbkdf2(DEFAULT_KDF_ITERATIONS.toUInt())
|
||||
return authSdkSource
|
||||
.makeRegisterKeys(
|
||||
email = email,
|
||||
password = masterPassword,
|
||||
kdf = kdf,
|
||||
)
|
||||
.flatMap { registerKeyResponse ->
|
||||
accountsService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
captchaResponse = captchaToken,
|
||||
key = registerKeyResponse.encryptedUserKey,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
encryptedPrivateKey = registerKeyResponse.keys.private,
|
||||
),
|
||||
kdfType = kdf.toKdfTypeJson(),
|
||||
kdfIterations = kdf.iterations,
|
||||
),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is RegisterResponseJson.CaptchaRequired -> {
|
||||
it.validationErrors.captchaKeys.firstOrNull()?.let { key ->
|
||||
RegisterResult.CaptchaRequired(captchaId = key)
|
||||
} ?: RegisterResult.Error(errorMessage = null)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Success -> {
|
||||
RegisterResult.Success(captchaToken = it.captchaBypassToken)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
RegisterResult.Error(errorMessage = null)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
|
||||
mutableCaptchaTokenFlow.tryEmit(tokenResult)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models high level auth state for the application.
|
|
@ -1,4 +1,4 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of logging in.
|
|
@ -0,0 +1,27 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of registering a new account.
|
||||
*/
|
||||
sealed class RegisterResult {
|
||||
/**
|
||||
* Register succeeded.
|
||||
*
|
||||
* @param captchaToken the captcha bypass token to bypass future captcha verifications.
|
||||
*/
|
||||
data class Success(val captchaToken: String) : RegisterResult()
|
||||
|
||||
/**
|
||||
* Captcha verification is required.
|
||||
*
|
||||
* @param captchaId the captcha id for performing the captcha verification.
|
||||
*/
|
||||
data class CaptchaRequired(val captchaId: String) : RegisterResult()
|
||||
|
||||
/**
|
||||
* There was an error logging in.
|
||||
*
|
||||
* @param errorMessage a message describing the error.
|
||||
*/
|
||||
data class Error(val errorMessage: String?) : RegisterResult()
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.util
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.net.URLEncoder
|
||||
|
@ -15,7 +14,7 @@ private const val CALLBACK_URI = "bitwarden://$CAPTCHA_HOST"
|
|||
/**
|
||||
* Generates a [Uri] to display a CAPTCHA challenge for Bitwarden authentication.
|
||||
*/
|
||||
fun LoginResult.CaptchaRequired.generateUriForCaptcha(): Uri {
|
||||
fun generateUriForCaptcha(captchaId: String): Uri {
|
||||
val json = buildJsonObject {
|
||||
put(key = "siteKey", value = captchaId)
|
||||
put(key = "locale", value = Locale.getDefault().toString())
|
|
@ -4,6 +4,7 @@ import androidx.navigation.NavController
|
|||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.navOptions
|
||||
import androidx.navigation.navigation
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestinations
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.navigateToCreateAccount
|
||||
|
@ -22,11 +23,25 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
|||
startDestination = LANDING_ROUTE,
|
||||
route = AUTH_GRAPH_ROUTE,
|
||||
) {
|
||||
createAccountDestinations(onNavigateBack = { navController.popBackStack() })
|
||||
createAccountDestinations(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToLogin = { emailAddress, captchaToken ->
|
||||
navController.navigateToLogin(
|
||||
emailAddress = emailAddress,
|
||||
captchaToken = captchaToken,
|
||||
navOptions = navOptions {
|
||||
popUpTo(LANDING_ROUTE)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
landingDestinations(
|
||||
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
|
||||
onNavigateToLogin = { emailAddress, regionLabel ->
|
||||
navController.navigateToLogin(emailAddress, regionLabel)
|
||||
onNavigateToLogin = { emailAddress ->
|
||||
navController.navigateToLogin(
|
||||
emailAddress = emailAddress,
|
||||
captchaToken = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
loginDestinations(
|
||||
|
|
|
@ -19,8 +19,12 @@ fun NavController.navigateToCreateAccount(navOptions: NavOptions? = null) {
|
|||
*/
|
||||
fun NavGraphBuilder.createAccountDestinations(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String, captchaToken: String) -> Unit,
|
||||
) {
|
||||
composable(route = CREATE_ACCOUNT_ROUTE) {
|
||||
CreateAccountScreen(onNavigateBack)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToLogin = onNavigateToLogin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.Navi
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButtonTopAppBar
|
||||
|
@ -70,6 +71,7 @@ import com.x8bit.bitwarden.ui.platform.theme.clickableSpanStyle
|
|||
@Composable
|
||||
fun CreateAccountScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String, captchaToken: String) -> Unit,
|
||||
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current),
|
||||
viewModel: CreateAccountViewModel = hiltViewModel(),
|
||||
) {
|
||||
|
@ -89,13 +91,26 @@ fun CreateAccountScreen(
|
|||
is CreateAccountEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
is CreateAccountEvent.NavigateToCaptcha -> {
|
||||
intentHandler.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
|
||||
is CreateAccountEvent.NavigateToLogin -> {
|
||||
onNavigateToLogin(
|
||||
event.email,
|
||||
event.captchaToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = state.errorDialogState,
|
||||
onDismissRequest = remember(viewModel) { { viewModel.trySendAction(ErrorDialogDismiss) } },
|
||||
)
|
||||
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = state.loadingDialogState,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
||||
|
@ -16,10 +21,12 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Ter
|
|||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -33,6 +40,7 @@ private const val MIN_PASSWORD_LENGTH = 12
|
|||
@HiltViewModel
|
||||
class CreateAccountViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val authRepository: AuthRepository,
|
||||
) : BaseViewModel<CreateAccountState, CreateAccountEvent, CreateAccountAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: CreateAccountState(
|
||||
|
@ -43,6 +51,7 @@ class CreateAccountViewModel @Inject constructor(
|
|||
isAcceptPoliciesToggled = false,
|
||||
isCheckDataBreachesToggled = false,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
),
|
||||
) {
|
||||
|
||||
|
@ -51,6 +60,16 @@ class CreateAccountViewModel @Inject constructor(
|
|||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
authRepository
|
||||
.captchaTokenResultFlow
|
||||
.onEach {
|
||||
sendAction(
|
||||
CreateAccountAction.Internal.ReceiveCaptchaToken(
|
||||
tokenResult = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: CreateAccountAction) {
|
||||
|
@ -66,6 +85,73 @@ class CreateAccountViewModel @Inject constructor(
|
|||
is CheckDataBreachesToggle -> handleCheckDataBreachesToggle(action)
|
||||
is PrivacyPolicyClick -> handlePrivacyPolicyClick()
|
||||
is TermsClick -> handleTermsClick()
|
||||
is CreateAccountAction.Internal.ReceiveRegisterResult -> {
|
||||
handleReceiveRegisterAccountResult(action)
|
||||
}
|
||||
|
||||
is CreateAccountAction.Internal.ReceiveCaptchaToken -> {
|
||||
handleReceiveCaptchaToken(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReceiveCaptchaToken(
|
||||
action: CreateAccountAction.Internal.ReceiveCaptchaToken,
|
||||
) {
|
||||
when (val result = action.tokenResult) {
|
||||
is CaptchaCallbackTokenResult.MissingToken -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
errorDialogState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is CaptchaCallbackTokenResult.Success -> {
|
||||
submitRegisterAccountRequest(captchaToken = result.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReceiveRegisterAccountResult(
|
||||
action: CreateAccountAction.Internal.ReceiveRegisterResult,
|
||||
) {
|
||||
when (val registerAccountResult = action.registerResult) {
|
||||
is RegisterResult.CaptchaRequired -> {
|
||||
mutableStateFlow.update { it.copy(loadingDialogState = LoadingDialogState.Hidden) }
|
||||
sendEvent(
|
||||
CreateAccountEvent.NavigateToCaptcha(
|
||||
uri = generateUriForCaptcha(captchaId = registerAccountResult.captchaId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResult.Error -> {
|
||||
// TODO show more robust error messages BIT-763
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
errorDialogState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = registerAccountResult.errorMessage?.asText()
|
||||
?: R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is RegisterResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(loadingDialogState = LoadingDialogState.Hidden) }
|
||||
sendEvent(
|
||||
CreateAccountEvent.NavigateToLogin(
|
||||
email = mutableStateFlow.value.emailInput,
|
||||
captchaToken = registerAccountResult.captchaToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,7 +207,30 @@ class CreateAccountViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
else -> {
|
||||
sendEvent(CreateAccountEvent.ShowToast("TODO: Handle Submit Click"))
|
||||
submitRegisterAccountRequest(captchaToken = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitRegisterAccountRequest(captchaToken: String?) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
loadingDialogState = LoadingDialogState.Shown(
|
||||
text = R.string.creating_account.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.register(
|
||||
email = mutableStateFlow.value.emailInput,
|
||||
masterPassword = mutableStateFlow.value.passwordInput,
|
||||
masterPasswordHint = mutableStateFlow.value.passwordHintInput.ifBlank { null },
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
sendAction(
|
||||
CreateAccountAction.Internal.ReceiveRegisterResult(
|
||||
registerResult = result,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,6 +247,7 @@ data class CreateAccountState(
|
|||
val isCheckDataBreachesToggled: Boolean,
|
||||
val isAcceptPoliciesToggled: Boolean,
|
||||
val errorDialogState: BasicDialogState,
|
||||
val loadingDialogState: LoadingDialogState,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
|
@ -155,6 +265,19 @@ sealed class CreateAccountEvent {
|
|||
*/
|
||||
data class ShowToast(val text: String) : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the captcha verification screen.
|
||||
*/
|
||||
data class NavigateToCaptcha(val uri: Uri) : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the captcha verification screen.
|
||||
*/
|
||||
data class NavigateToLogin(
|
||||
val email: String,
|
||||
val captchaToken: String,
|
||||
) : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Navigate to terms and conditions.
|
||||
*/
|
||||
|
@ -224,4 +347,23 @@ sealed class CreateAccountAction {
|
|||
* User tapped terms link.
|
||||
*/
|
||||
data object TermsClick : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [CreateAccountViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : CreateAccountAction() {
|
||||
/**
|
||||
* Indicates a captcha callback token has been received.
|
||||
*/
|
||||
data class ReceiveCaptchaToken(
|
||||
val tokenResult: CaptchaCallbackTokenResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a [RegisterResult] has been received.
|
||||
*/
|
||||
data class ReceiveRegisterResult(
|
||||
val registerResult: RegisterResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ fun NavController.navigateToLanding(navOptions: NavOptions? = null) {
|
|||
*/
|
||||
fun NavGraphBuilder.landingDestinations(
|
||||
onNavigateToCreateAccount: () -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String, regionLabel: String) -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String) -> Unit,
|
||||
) {
|
||||
composable(route = LANDING_ROUTE) {
|
||||
LandingScreen(
|
||||
|
|
|
@ -54,7 +54,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
|||
@Suppress("LongMethod")
|
||||
fun LandingScreen(
|
||||
onNavigateToCreateAccount: () -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String, regionLabel: String) -> Unit,
|
||||
onNavigateToLogin: (emailAddress: String) -> Unit,
|
||||
viewModel: LandingViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
@ -63,7 +63,6 @@ fun LandingScreen(
|
|||
LandingEvent.NavigateToCreateAccount -> onNavigateToCreateAccount()
|
||||
is LandingEvent.NavigateToLogin -> onNavigateToLogin(
|
||||
event.emailAddress,
|
||||
event.regionLabel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,12 +66,13 @@ class LandingViewModel @Inject constructor(
|
|||
|
||||
val email = mutableStateFlow.value.emailInput
|
||||
val isRememberMeEnabled = mutableStateFlow.value.isRememberMeEnabled
|
||||
val selectedRegionLabel = mutableStateFlow.value.selectedRegion.label
|
||||
|
||||
// Update the remembered email address
|
||||
authRepository.rememberedEmailAddress = email.takeUnless { !isRememberMeEnabled }
|
||||
// Update the selected region selectedRegionLabel
|
||||
authRepository.selectedRegionLabel = mutableStateFlow.value.selectedRegion.label
|
||||
|
||||
sendEvent(LandingEvent.NavigateToLogin(email, selectedRegionLabel))
|
||||
sendEvent(LandingEvent.NavigateToLogin(email))
|
||||
}
|
||||
|
||||
private fun handleCreateAccountClicked() {
|
||||
|
@ -125,7 +126,6 @@ sealed class LandingEvent {
|
|||
*/
|
||||
data class NavigateToLogin(
|
||||
val emailAddress: String,
|
||||
val regionLabel: String,
|
||||
) : LandingEvent()
|
||||
}
|
||||
|
||||
|
|
|
@ -9,16 +9,16 @@ import androidx.navigation.compose.composable
|
|||
import androidx.navigation.navArgument
|
||||
|
||||
private const val EMAIL_ADDRESS: String = "email_address"
|
||||
private const val REGION_LABEL: String = "region_label"
|
||||
private const val LOGIN_ROUTE: String = "login/{$EMAIL_ADDRESS}/{$REGION_LABEL}"
|
||||
private const val CAPTCHA_TOKEN = "captcha_token"
|
||||
private const val LOGIN_ROUTE: String = "login/{$EMAIL_ADDRESS}/{$CAPTCHA_TOKEN}"
|
||||
|
||||
/**
|
||||
* Class to retrieve login arguments from the [SavedStateHandle].
|
||||
*/
|
||||
class LoginArgs(val emailAddress: String, val regionLabel: String) {
|
||||
class LoginArgs(val emailAddress: String, val captchaToken: String?) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
|
||||
checkNotNull(savedStateHandle[REGION_LABEL]) as String,
|
||||
savedStateHandle[CAPTCHA_TOKEN],
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -27,10 +27,10 @@ class LoginArgs(val emailAddress: String, val regionLabel: String) {
|
|||
*/
|
||||
fun NavController.navigateToLogin(
|
||||
emailAddress: String,
|
||||
regionLabel: String,
|
||||
captchaToken: String?,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate("login/$emailAddress/$regionLabel", navOptions)
|
||||
this.navigate("login/$emailAddress/$captchaToken", navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,9 +7,9 @@ import android.os.Parcelable
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -38,9 +38,10 @@ class LoginViewModel @Inject constructor(
|
|||
emailAddress = LoginArgs(savedStateHandle).emailAddress,
|
||||
isLoginButtonEnabled = true,
|
||||
passwordInput = "",
|
||||
region = LoginArgs(savedStateHandle).regionLabel,
|
||||
region = authRepository.selectedRegionLabel,
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
captchaToken = LoginArgs(savedStateHandle).captchaToken,
|
||||
),
|
||||
) {
|
||||
|
||||
|
@ -84,7 +85,7 @@ class LoginViewModel @Inject constructor(
|
|||
mutableStateFlow.update { it.copy(loadingDialogState = LoadingDialogState.Hidden) }
|
||||
sendEvent(
|
||||
event = LoginEvent.NavigateToCaptcha(
|
||||
uri = loginResult.generateUriForCaptcha(),
|
||||
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -123,7 +124,12 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
is CaptchaCallbackTokenResult.Success -> attemptLogin(captchaToken = tokenResult.token)
|
||||
is CaptchaCallbackTokenResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(captchaToken = tokenResult.token)
|
||||
}
|
||||
attemptLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,10 +138,10 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleLoginButtonClicked() {
|
||||
attemptLogin(captchaToken = null)
|
||||
attemptLogin()
|
||||
}
|
||||
|
||||
private fun attemptLogin(captchaToken: String?) {
|
||||
private fun attemptLogin() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
loadingDialogState = LoadingDialogState.Shown(
|
||||
|
@ -147,7 +153,7 @@ class LoginViewModel @Inject constructor(
|
|||
val result = authRepository.login(
|
||||
email = mutableStateFlow.value.emailAddress,
|
||||
password = mutableStateFlow.value.passwordInput,
|
||||
captchaToken = captchaToken,
|
||||
captchaToken = mutableStateFlow.value.captchaToken,
|
||||
)
|
||||
sendAction(
|
||||
LoginAction.Internal.ReceiveLoginResult(
|
||||
|
@ -183,6 +189,7 @@ class LoginViewModel @Inject constructor(
|
|||
data class LoginState(
|
||||
val passwordInput: String,
|
||||
val emailAddress: String,
|
||||
val captchaToken: String?,
|
||||
val region: String,
|
||||
val isLoginButtonEnabled: Boolean,
|
||||
val loadingDialogState: LoadingDialogState,
|
||||
|
|
|
@ -3,7 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.PBKDF2_SHA256
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.create
|
||||
|
||||
class AccountsServiceTest : BaseServiceTest() {
|
||||
|
||||
private val accountsApi: AccountsApi = retrofit.create()
|
||||
private val service = AccountsServiceImpl(accountsApi)
|
||||
private val service = AccountsServiceImpl(
|
||||
accountsApi = accountsApi,
|
||||
json = Json,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `preLogin with unknown kdf type be failure`() = runTest {
|
||||
|
@ -93,7 +101,77 @@ class AccountsServiceTest : BaseServiceTest() {
|
|||
assertEquals(Result.success(expectedResponse), service.preLogin(EMAIL))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register success json should be Success`() = runTest {
|
||||
val json = """
|
||||
{
|
||||
"captchaBypassToken": "mock_token"
|
||||
}
|
||||
"""
|
||||
val expectedResponse = RegisterResponseJson.Success(
|
||||
captchaBypassToken = "mock_token",
|
||||
)
|
||||
val response = MockResponse().setBody(json)
|
||||
server.enqueue(response)
|
||||
assertEquals(Result.success(expectedResponse), service.register(registerRequestBody))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register failure json should be failure`() = runTest {
|
||||
val json = """
|
||||
{
|
||||
"message": "The model state is invalid.",
|
||||
"validationErrors": {
|
||||
"": [
|
||||
"Email '' is already taken."
|
||||
]
|
||||
},
|
||||
"exceptionMessage": null,
|
||||
"exceptionStackTrace": null,
|
||||
"innerExceptionMessage": null,
|
||||
"object": "error"
|
||||
}
|
||||
"""
|
||||
val response = MockResponse().setResponseCode(400).setBody(json)
|
||||
server.enqueue(response)
|
||||
assertTrue(service.register(registerRequestBody).isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register captcha json should be CaptchaRequired`() = runTest {
|
||||
val json = """
|
||||
{
|
||||
"validationErrors": {
|
||||
"HCaptcha_SiteKey": [
|
||||
"mock_token"
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
val expectedResponse = RegisterResponseJson.CaptchaRequired(
|
||||
validationErrors = RegisterResponseJson.CaptchaRequired.ValidationErrors(
|
||||
captchaKeys = listOf("mock_token"),
|
||||
),
|
||||
)
|
||||
val response = MockResponse().setResponseCode(400).setBody(json)
|
||||
server.enqueue(response)
|
||||
assertEquals(Result.success(expectedResponse), service.register(registerRequestBody))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "email"
|
||||
private val registerRequestBody = RegisterRequestJson(
|
||||
email = EMAIL,
|
||||
masterPasswordHash = "mockk_masterPasswordHash",
|
||||
masterPasswordHint = "mockk_masterPasswordHint",
|
||||
captchaResponse = "mockk_captchaResponse",
|
||||
key = "mockk_key",
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = "mockk_publicKey",
|
||||
encryptedPrivateKey = "mockk_encryptedPrivateKey",
|
||||
),
|
||||
kdfType = PBKDF2_SHA256,
|
||||
kdfIterations = 600000U,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RsaKeyPair
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson.PBKDF2_SHA256
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import io.mockk.clearMocks
|
||||
|
@ -38,6 +45,22 @@ class AuthRepositoryTest {
|
|||
kdf = PRE_LOGIN_SUCCESS.kdfParams.toSdkParams(),
|
||||
)
|
||||
} returns Result.success(PASSWORD_HASH)
|
||||
coEvery {
|
||||
makeRegisterKeys(
|
||||
email = EMAIL,
|
||||
password = PASSWORD,
|
||||
kdf = Kdf.Pbkdf2(DEFAULT_KDF_ITERATIONS.toUInt()),
|
||||
)
|
||||
} returns Result.success(
|
||||
RegisterKeyResponse(
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
encryptedUserKey = ENCRYPTED_USER_KEY,
|
||||
keys = RsaKeyPair(
|
||||
public = PUBLIC_KEY,
|
||||
private = PRIVATE_KEY,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private val repository = AuthRepositoryImpl(
|
||||
|
@ -116,16 +139,14 @@ class AuthRepositoryTest {
|
|||
passwordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
.returns(
|
||||
Result.success(
|
||||
GetTokenResponseJson.Invalid(
|
||||
errorModel = GetTokenResponseJson.Invalid.ErrorModel(
|
||||
errorMessage = "mock_error_message",
|
||||
),
|
||||
),
|
||||
} returns Result.success(
|
||||
GetTokenResponseJson.Invalid(
|
||||
errorModel = GetTokenResponseJson.Invalid.ErrorModel(
|
||||
errorMessage = "mock_error_message",
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
assertEquals(LoginResult.Error(errorMessage = "mock_error_message"), result)
|
||||
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
|
||||
|
@ -194,6 +215,144 @@ class AuthRepositoryTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register Success should return Success`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
accountsService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = EMAIL,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
masterPasswordHint = null,
|
||||
captchaResponse = null,
|
||||
key = ENCRYPTED_USER_KEY,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = PUBLIC_KEY,
|
||||
encryptedPrivateKey = PRIVATE_KEY,
|
||||
),
|
||||
kdfType = PBKDF2_SHA256,
|
||||
kdfIterations = DEFAULT_KDF_ITERATIONS.toUInt(),
|
||||
),
|
||||
)
|
||||
} returns Result.success(RegisterResponseJson.Success(captchaBypassToken = CAPTCHA_KEY))
|
||||
|
||||
val result = repository.register(
|
||||
email = EMAIL,
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertEquals(RegisterResult.Success(CAPTCHA_KEY), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register returns CaptchaRequired captchaKeys empty should return Error no message`() =
|
||||
runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
accountsService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = EMAIL,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
masterPasswordHint = null,
|
||||
captchaResponse = null,
|
||||
key = ENCRYPTED_USER_KEY,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = PUBLIC_KEY,
|
||||
encryptedPrivateKey = PRIVATE_KEY,
|
||||
),
|
||||
kdfType = PBKDF2_SHA256,
|
||||
kdfIterations = DEFAULT_KDF_ITERATIONS.toUInt(),
|
||||
),
|
||||
)
|
||||
} returns Result.success(
|
||||
RegisterResponseJson.CaptchaRequired(
|
||||
validationErrors = RegisterResponseJson
|
||||
.CaptchaRequired
|
||||
.ValidationErrors(
|
||||
captchaKeys = emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = repository.register(
|
||||
email = EMAIL,
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertEquals(RegisterResult.Error(errorMessage = null), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register returns CaptchaRequired captchaKeys should return CaptchaRequired`() =
|
||||
runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
accountsService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = EMAIL,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
masterPasswordHint = null,
|
||||
captchaResponse = null,
|
||||
key = ENCRYPTED_USER_KEY,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = PUBLIC_KEY,
|
||||
encryptedPrivateKey = PRIVATE_KEY,
|
||||
),
|
||||
kdfType = PBKDF2_SHA256,
|
||||
kdfIterations = DEFAULT_KDF_ITERATIONS.toUInt(),
|
||||
),
|
||||
)
|
||||
} returns Result.success(
|
||||
RegisterResponseJson.CaptchaRequired(
|
||||
validationErrors = RegisterResponseJson
|
||||
.CaptchaRequired
|
||||
.ValidationErrors(
|
||||
captchaKeys = listOf(CAPTCHA_KEY),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = repository.register(
|
||||
email = EMAIL,
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertEquals(RegisterResult.CaptchaRequired(captchaId = CAPTCHA_KEY), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `register Failure should return Error with no message`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery {
|
||||
accountsService.register(
|
||||
body = RegisterRequestJson(
|
||||
email = EMAIL,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
masterPasswordHint = null,
|
||||
captchaResponse = null,
|
||||
key = ENCRYPTED_USER_KEY,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = PUBLIC_KEY,
|
||||
encryptedPrivateKey = PRIVATE_KEY,
|
||||
),
|
||||
kdfType = PBKDF2_SHA256,
|
||||
kdfIterations = DEFAULT_KDF_ITERATIONS.toUInt(),
|
||||
),
|
||||
)
|
||||
} returns Result.failure(RuntimeException())
|
||||
|
||||
val result = repository.register(
|
||||
email = EMAIL,
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertEquals(RegisterResult.Error(errorMessage = null), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setCaptchaCallbackToken should change the value of captchaTokenFlow`() = runTest {
|
||||
repository.captchaTokenResultFlow.test {
|
||||
|
@ -240,6 +399,10 @@ class AuthRepositoryTest {
|
|||
private const val PASSWORD_HASH = "passwordHash"
|
||||
private const val ACCESS_TOKEN = "accessToken"
|
||||
private const val CAPTCHA_KEY = "captcha"
|
||||
private const val DEFAULT_KDF_ITERATIONS = 600000
|
||||
private const val ENCRYPTED_USER_KEY = "encryptedUserKey"
|
||||
private const val PUBLIC_KEY = "PublicKey"
|
||||
private const val PRIVATE_KEY = "privateKey"
|
||||
private val PRE_LOGIN_SUCCESS = PreLoginResponseJson(
|
||||
kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(iterations = 1u),
|
||||
)
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.util
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class LoginResultExtensionsTest : BaseComposeTest() {
|
||||
class CaptchaUtilsTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `generateIntentForCaptcha should return valid Uri`() {
|
||||
val captchaRequired = LoginResult.CaptchaRequired("testCaptchaId")
|
||||
val actualUri = captchaRequired.generateUriForCaptcha()
|
||||
val actualUri = generateUriForCaptcha(captchaId = "testCaptchaId")
|
||||
val expectedUrl = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||
"?data=eyJzaXRlS2V5IjoidGVzdENhcHRjaGFJZCIsImxvY2FsZSI6ImVuX1VTIiwiY2Fsb" +
|
||||
"GJhY2tVcmkiOiJiaXR3YXJkZW46Ly9jYXB0Y2hhLWNhbGxiYWNrIiwiY2FwdGNoYVJlcXVp" +
|
|
@ -27,12 +27,14 @@ import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class CreateAccountScreenTest : BaseComposeTest() {
|
||||
|
@ -45,7 +47,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(SubmitClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Submit").performClick()
|
||||
verify { viewModel.trySendAction(SubmitClick) }
|
||||
|
@ -59,7 +65,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(CloseClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||
verify { viewModel.trySendAction(CloseClick) }
|
||||
|
@ -73,7 +83,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(CheckDataBreachesToggle(true)) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Check known data breaches for this password")
|
||||
|
@ -90,7 +104,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(AcceptPoliciesToggle(true)) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("By activating this switch you agree", substring = true)
|
||||
|
@ -108,9 +126,63 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { eventFlow } returns flowOf(CreateAccountEvent.NavigateBack)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = onNavigateBack, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToLogin event should invoke navigate login lambda`() {
|
||||
var onNavigateToLoginCalled = false
|
||||
val onNavigateToLogin = { _: String, _: String -> onNavigateToLoginCalled = true }
|
||||
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns flowOf(
|
||||
CreateAccountEvent.NavigateToLogin(
|
||||
email = "",
|
||||
captchaToken = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = onNavigateToLogin,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
assertTrue(onNavigateToLoginCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToCaptcha event should invoke intent handler`() {
|
||||
val mockUri = mockk<Uri>()
|
||||
val intentHandler = mockk<IntentHandler>(relaxed = true) {
|
||||
every { startCustomTabsActivity(mockUri) } returns Unit
|
||||
}
|
||||
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns flowOf(
|
||||
CreateAccountEvent.NavigateToCaptcha(
|
||||
uri = mockUri,
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
intentHandler = intentHandler,
|
||||
)
|
||||
}
|
||||
verify {
|
||||
intentHandler.startCustomTabsActivity(mockUri)
|
||||
}
|
||||
assert(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -127,6 +199,7 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
intentHandler = intentHandler,
|
||||
)
|
||||
|
@ -150,6 +223,7 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
intentHandler = intentHandler,
|
||||
)
|
||||
|
@ -167,7 +241,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(EmailInputChange("input")) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Email address").performTextInput(TEST_INPUT)
|
||||
verify { viewModel.trySendAction(EmailInputChange(TEST_INPUT)) }
|
||||
|
@ -181,7 +259,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(PasswordInputChange("input")) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Master password").performTextInput(TEST_INPUT)
|
||||
verify { viewModel.trySendAction(PasswordInputChange(TEST_INPUT)) }
|
||||
|
@ -195,7 +277,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(ConfirmPasswordInputChange("input")) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Re-type master password").performTextInput(TEST_INPUT)
|
||||
verify { viewModel.trySendAction(ConfirmPasswordInputChange(TEST_INPUT)) }
|
||||
|
@ -209,7 +295,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(PasswordHintChange("input")) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Master password hint (optional)")
|
||||
|
@ -232,7 +322,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(CreateAccountAction.ErrorDialogDismiss) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Ok")
|
||||
|
@ -256,7 +350,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { trySendAction(CreateAccountAction.ErrorDialogDismiss) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNode(isDialog()).assertIsDisplayed()
|
||||
}
|
||||
|
@ -268,7 +366,11 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
every { eventFlow } returns emptyFlow()
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
// should start with 2 Show buttons:
|
||||
|
@ -306,6 +408,7 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
isCheckDataBreachesToggled = false,
|
||||
isAcceptPoliciesToggled = false,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,57 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import app.cash.turbine.testIn
|
||||
import app.cash.turbine.turbineScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.ShowToast
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CreateAccountViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mockAuthRepository = mockk<AuthRepository> {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(LOGIN_RESULT_PATH)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(LOGIN_RESULT_PATH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
|
@ -34,15 +65,22 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
isCheckDataBreachesToggled = false,
|
||||
isAcceptPoliciesToggled = false,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
)
|
||||
val handle = SavedStateHandle(mapOf("state" to savedState))
|
||||
val viewModel = CreateAccountViewModel(handle)
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = handle,
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
assertEquals(savedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubmitClick with password below 12 chars should show password length dialog`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
val input = "abcdefghikl"
|
||||
viewModel.trySendAction(PasswordInputChange("abcdefghikl"))
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
|
@ -59,18 +97,181 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `SubmitClick with long enough password emit ShowToast`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
fun `SubmitClick with long enough password should show and hide loading dialog`() = runTest {
|
||||
val repo = mockk<AuthRepository> {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
coEvery {
|
||||
register(
|
||||
email = "",
|
||||
masterPassword = "longenoughpassword",
|
||||
masterPasswordHint = null,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns RegisterResult.Success(captchaToken = "mock_token")
|
||||
}
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = repo,
|
||||
)
|
||||
|
||||
turbineScope {
|
||||
val stateFlow = viewModel.stateFlow.testIn(backgroundScope)
|
||||
val eventFlow = viewModel.eventFlow.testIn(backgroundScope)
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(PasswordInputChange("longenoughpassword"))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(passwordInput = "longenoughpassword"),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.SubmitClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordInput = "longenoughpassword",
|
||||
loadingDialogState = LoadingDialogState.Shown(
|
||||
text = R.string.creating_account.asText(),
|
||||
),
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
CreateAccountEvent.NavigateToLogin(
|
||||
email = "",
|
||||
captchaToken = "mock_token",
|
||||
),
|
||||
eventFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordInput = "longenoughpassword",
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubmitClick register returns error should update errorDialogState`() = runTest {
|
||||
val repo = mockk<AuthRepository> {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
coEvery {
|
||||
register(
|
||||
email = "",
|
||||
masterPassword = "longenoughpassword",
|
||||
masterPasswordHint = null,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns RegisterResult.Error(errorMessage = "mock_error")
|
||||
}
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = repo,
|
||||
)
|
||||
viewModel.trySendAction(PasswordInputChange("longenoughpassword"))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(passwordInput = "longenoughpassword"),
|
||||
awaitItem(),
|
||||
)
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.SubmitClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordInput = "longenoughpassword",
|
||||
loadingDialogState = LoadingDialogState.Shown(
|
||||
text = R.string.creating_account.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
passwordInput = "longenoughpassword",
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
errorDialogState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = "mock_error".asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubmitClick register returns CaptchaRequired should emit NavigateToCaptcha`() = runTest {
|
||||
val mockkUri = mockk<Uri>()
|
||||
every {
|
||||
generateUriForCaptcha(captchaId = "mock_captcha_id")
|
||||
} returns mockkUri
|
||||
val repo = mockk<AuthRepository> {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
coEvery {
|
||||
register(
|
||||
email = "",
|
||||
masterPassword = "longenoughpassword",
|
||||
masterPasswordHint = null,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns RegisterResult.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||
}
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = repo,
|
||||
)
|
||||
viewModel.trySendAction(PasswordInputChange("longenoughpassword"))
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.SubmitClick)
|
||||
assertEquals(ShowToast("TODO: Handle Submit Click"), awaitItem())
|
||||
assertEquals(
|
||||
CreateAccountEvent.NavigateToCaptcha(uri = mockkUri),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SubmitClick register returns Success should emit NavigateToLogin`() = runTest {
|
||||
val mockkUri = mockk<Uri>()
|
||||
every {
|
||||
generateUriForCaptcha(captchaId = "mock_captcha_id")
|
||||
} returns mockkUri
|
||||
val repo = mockk<AuthRepository> {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
coEvery {
|
||||
register(
|
||||
email = "",
|
||||
masterPassword = "longenoughpassword",
|
||||
masterPasswordHint = null,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns RegisterResult.Success(captchaToken = "mock_captcha_token")
|
||||
}
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = repo,
|
||||
)
|
||||
viewModel.trySendAction(PasswordInputChange("longenoughpassword"))
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.SubmitClick)
|
||||
assertEquals(
|
||||
CreateAccountEvent.NavigateToLogin(
|
||||
email = "",
|
||||
captchaToken = "mock_captcha_token",
|
||||
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CloseClick)
|
||||
assertEquals(CreateAccountEvent.NavigateBack, awaitItem())
|
||||
|
@ -79,7 +280,10 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `PrivacyPolicyClick should emit NavigatePrivacyPolicy`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.PrivacyPolicyClick)
|
||||
assertEquals(CreateAccountEvent.NavigateToPrivacyPolicy, awaitItem())
|
||||
|
@ -88,7 +292,10 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `TermsClick should emit NavigateToTerms`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.TermsClick)
|
||||
assertEquals(CreateAccountEvent.NavigateToTerms, awaitItem())
|
||||
|
@ -97,7 +304,10 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `ConfirmPasswordInputChange update passwordInput`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
viewModel.actionChannel.trySend(ConfirmPasswordInputChange("input"))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE.copy(confirmPasswordInput = "input"), awaitItem())
|
||||
|
@ -106,7 +316,10 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `EmailInputChange update passwordInput`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
viewModel.actionChannel.trySend(EmailInputChange("input"))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE.copy(emailInput = "input"), awaitItem())
|
||||
|
@ -115,7 +328,10 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `PasswordHintChange update passwordInput`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
viewModel.actionChannel.trySend(PasswordHintChange("input"))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE.copy(passwordHintInput = "input"), awaitItem())
|
||||
|
@ -124,7 +340,10 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `PasswordInputChange update passwordInput`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
viewModel.actionChannel.trySend(PasswordInputChange("input"))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE.copy(passwordInput = "input"), awaitItem())
|
||||
|
@ -133,7 +352,10 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `CheckDataBreachesToggle should change isCheckDataBreachesToggled`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
viewModel.trySendAction(CreateAccountAction.CheckDataBreachesToggle(true))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE.copy(isCheckDataBreachesToggled = true), awaitItem())
|
||||
|
@ -142,7 +364,10 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `AcceptPoliciesToggle should change isAcceptPoliciesToggled`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
val viewModel = CreateAccountViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
)
|
||||
viewModel.trySendAction(CreateAccountAction.AcceptPoliciesToggle(true))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE.copy(isAcceptPoliciesToggled = true), awaitItem())
|
||||
|
@ -158,6 +383,9 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
isCheckDataBreachesToggled = false,
|
||||
isAcceptPoliciesToggled = false,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
)
|
||||
private const val LOGIN_RESULT_PATH =
|
||||
"com.x8bit.bitwarden.data.auth.repository.util.CaptchaUtilsKt"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
onNavigateToLogin = { _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
onNavigateToLogin = { _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
onNavigateToLogin = { _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
onNavigateToLogin = { _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
onNavigateToLogin = { _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = { onNavigateToCreateAccountCalled = true },
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
onNavigateToLogin = { _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
@ -180,13 +180,11 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
@Test
|
||||
fun `NavigateToLogin event should call onNavigateToLogin`() {
|
||||
val testEmail = "test@test.com"
|
||||
val testRegion = "bitwarden.com"
|
||||
|
||||
var capturedEmail: String? = null
|
||||
var capturedRegion: String? = null
|
||||
|
||||
val viewModel = mockk<LandingViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns flowOf(LandingEvent.NavigateToLogin(testEmail, testRegion))
|
||||
every { eventFlow } returns flowOf(LandingEvent.NavigateToLogin(testEmail))
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
LandingState(
|
||||
emailInput = "",
|
||||
|
@ -200,16 +198,14 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = { },
|
||||
onNavigateToLogin = { email, region ->
|
||||
onNavigateToLogin = { email ->
|
||||
capturedEmail = email
|
||||
capturedRegion = region
|
||||
},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(testEmail, capturedEmail)
|
||||
assertEquals(testRegion, capturedRegion)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -230,7 +226,7 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
composeTestRule.setContent {
|
||||
LandingScreen(
|
||||
onNavigateToCreateAccount = {},
|
||||
onNavigateToLogin = { _, _ -> },
|
||||
onNavigateToLogin = { _ -> },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LandingAction.ContinueButtonClick)
|
||||
assertEquals(
|
||||
LandingEvent.NavigateToLogin("input", "bitwarden.com"),
|
||||
LandingEvent.NavigateToLogin("input"),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
captchaToken = null,
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
region = "",
|
||||
|
@ -61,6 +62,7 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
captchaToken = null,
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
region = "",
|
||||
|
@ -88,6 +90,7 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
captchaToken = null,
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
region = "",
|
||||
|
@ -115,6 +118,7 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
captchaToken = null,
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
region = "",
|
||||
|
@ -154,6 +158,7 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
captchaToken = null,
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
region = "",
|
||||
|
@ -182,6 +187,7 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
captchaToken = null,
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
region = "",
|
||||
|
@ -210,6 +216,7 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
captchaToken = null,
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
region = "",
|
||||
|
|
|
@ -4,9 +4,9 @@ import android.net.Uri
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -47,6 +47,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
@ -83,6 +84,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
@ -106,6 +108,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
} returns LoginResult.Error(errorMessage = "mock_error")
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
}
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
|
@ -145,6 +148,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
login("test@gmail.com", "", captchaToken = null)
|
||||
} returns LoginResult.Success
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
}
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
|
@ -176,14 +180,13 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val mockkUri = mockk<Uri>()
|
||||
every {
|
||||
LoginResult
|
||||
.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||
.generateUriForCaptcha()
|
||||
generateUriForCaptcha(captchaId = "mock_captcha_id")
|
||||
} returns mockkUri
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
coEvery { login("test@gmail.com", "", captchaToken = null) } returns
|
||||
LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
}
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
|
@ -204,6 +207,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
@ -222,6 +226,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
@ -240,6 +245,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
@ -258,6 +264,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
@ -276,6 +283,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
every { captchaTokenResultFlow } returns flowOf(
|
||||
CaptchaCallbackTokenResult.Success("token"),
|
||||
)
|
||||
every { selectedRegionLabel } returns "bitwarden.us"
|
||||
coEvery {
|
||||
login(
|
||||
"test@gmail.com",
|
||||
|
@ -298,12 +306,13 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
emailAddress = "test@gmail.com",
|
||||
passwordInput = "",
|
||||
isLoginButtonEnabled = true,
|
||||
region = "",
|
||||
region = "bitwarden.us",
|
||||
loadingDialogState = LoadingDialogState.Hidden,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
captchaToken = null,
|
||||
)
|
||||
|
||||
private const val LOGIN_RESULT_PATH =
|
||||
"com.x8bit.bitwarden.data.auth.datasource.network.util.LoginResultExtensionsKt"
|
||||
"com.x8bit.bitwarden.data.auth.repository.util.CaptchaUtilsKt"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState.Authenticated
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState.Unauthenticated
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState.Authenticated
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState.Unauthenticated
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
|
|
Loading…
Reference in a new issue