mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
[PM-24642] Remove captcha connector code (#5677)
This commit is contained in:
parent
29243c8f44
commit
694865c213
@ -133,16 +133,6 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="captcha-callback"
|
||||
android:scheme="bitwarden" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="duo-callback"
|
||||
android:scheme="bitwarden" />
|
||||
|
||||
@ -3,7 +3,6 @@ package com.x8bit.bitwarden
|
||||
import android.content.Intent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
|
||||
@ -27,7 +26,6 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
private fun handleIntentReceived(action: AuthCallbackAction.IntentReceive) {
|
||||
val yubiKeyResult = action.intent.getYubiKeyResultOrNull()
|
||||
val webAuthResult = action.intent.getWebAuthResultOrNull()
|
||||
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
|
||||
val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult()
|
||||
val ssoCallbackResult = action.intent.getSsoCallbackResult()
|
||||
when {
|
||||
@ -35,12 +33,6 @@ class AuthCallbackViewModel @Inject constructor(
|
||||
authRepository.setYubiKeyResult(yubiKeyResult = yubiKeyResult)
|
||||
}
|
||||
|
||||
captchaCallbackTokenResult != null -> {
|
||||
authRepository.setCaptchaCallbackTokenResult(
|
||||
tokenResult = captchaCallbackTokenResult,
|
||||
)
|
||||
}
|
||||
|
||||
duoCallbackTokenResult != null -> {
|
||||
authRepository.setDuoCallbackTokenResult(
|
||||
tokenResult = duoCallbackTokenResult,
|
||||
|
||||
@ -32,7 +32,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
@ -56,12 +55,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
*/
|
||||
val userStateFlow: StateFlow<UserState?>
|
||||
|
||||
/**
|
||||
* Flow of the current [CaptchaCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setCaptchaCallbackTokenResult] is called.
|
||||
*/
|
||||
val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult>
|
||||
|
||||
/**
|
||||
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
|
||||
@ -186,7 +179,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
captchaToken: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
@ -201,7 +193,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
asymmetricalKey: String,
|
||||
requestPrivateKey: String,
|
||||
masterPasswordHash: String?,
|
||||
captchaToken: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
@ -213,7 +204,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
@ -226,7 +216,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
ssoCode: String,
|
||||
ssoCodeVerifier: String,
|
||||
ssoRedirectUri: String,
|
||||
captchaToken: String?,
|
||||
organizationIdentifier: String,
|
||||
): LoginResult
|
||||
|
||||
@ -239,7 +228,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult
|
||||
|
||||
@ -294,7 +282,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String? = null,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
): RegisterResult
|
||||
@ -332,11 +319,6 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
passwordHint: String?,
|
||||
): SetPasswordResult
|
||||
|
||||
/**
|
||||
* Set the value of [captchaTokenResultFlow].
|
||||
*/
|
||||
fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult)
|
||||
|
||||
/**
|
||||
* Set the value of [duoTokenResultFlow].
|
||||
*/
|
||||
|
||||
@ -87,7 +87,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
@ -331,10 +330,6 @@ class AuthRepositoryImpl(
|
||||
),
|
||||
)
|
||||
|
||||
private val captchaTokenChannel = Channel<CaptchaCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||
override val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult> =
|
||||
captchaTokenChannel.receiveAsFlow()
|
||||
|
||||
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> = duoTokenChannel.receiveAsFlow()
|
||||
|
||||
@ -619,7 +614,6 @@ class AuthRepositoryImpl(
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
captchaToken: String?,
|
||||
): LoginResult = identityService
|
||||
.preLogin(email = email)
|
||||
.flatMap {
|
||||
@ -638,7 +632,6 @@ class AuthRepositoryImpl(
|
||||
username = email,
|
||||
password = passwordHash,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
@ -658,7 +651,6 @@ class AuthRepositoryImpl(
|
||||
asymmetricalKey: String,
|
||||
requestPrivateKey: String,
|
||||
masterPasswordHash: String?,
|
||||
captchaToken: String?,
|
||||
): LoginResult =
|
||||
loginCommon(
|
||||
email = email,
|
||||
@ -673,14 +665,12 @@ class AuthRepositoryImpl(
|
||||
asymmetricalKey = asymmetricalKey,
|
||||
privateKey = requestPrivateKey,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String?,
|
||||
twoFactorData: TwoFactorDataModel,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
@ -689,7 +679,6 @@ class AuthRepositoryImpl(
|
||||
password = password,
|
||||
authModel = it,
|
||||
twoFactorData = twoFactorData,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
@ -703,7 +692,6 @@ class AuthRepositoryImpl(
|
||||
email: String,
|
||||
password: String?,
|
||||
newDeviceOtp: String,
|
||||
captchaToken: String?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = identityTokenAuthModel
|
||||
?.let {
|
||||
@ -712,7 +700,6 @@ class AuthRepositoryImpl(
|
||||
password = password,
|
||||
authModel = it,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
|
||||
deviceData = twoFactorDeviceData,
|
||||
orgIdentifier = orgIdentifier,
|
||||
)
|
||||
@ -746,7 +733,6 @@ class AuthRepositoryImpl(
|
||||
ssoCode: String,
|
||||
ssoCodeVerifier: String,
|
||||
ssoRedirectUri: String,
|
||||
captchaToken: String?,
|
||||
organizationIdentifier: String,
|
||||
): LoginResult = loginCommon(
|
||||
email = email,
|
||||
@ -755,7 +741,6 @@ class AuthRepositoryImpl(
|
||||
ssoCodeVerifier = ssoCodeVerifier,
|
||||
ssoRedirectUri = ssoRedirectUri,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
orgIdentifier = organizationIdentifier,
|
||||
)
|
||||
|
||||
@ -905,7 +890,6 @@ class AuthRepositoryImpl(
|
||||
masterPassword: String,
|
||||
masterPasswordHint: String?,
|
||||
emailVerificationToken: String?,
|
||||
captchaToken: String?,
|
||||
shouldCheckDataBreaches: Boolean,
|
||||
isMasterPasswordStrong: Boolean,
|
||||
): RegisterResult {
|
||||
@ -940,7 +924,6 @@ class AuthRepositoryImpl(
|
||||
email = email,
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
captchaResponse = captchaToken,
|
||||
key = registerKeyResponse.encryptedUserKey,
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
@ -957,7 +940,6 @@ class AuthRepositoryImpl(
|
||||
masterPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||
masterPasswordHint = masterPasswordHint,
|
||||
emailVerificationToken = emailVerificationToken,
|
||||
captchaResponse = captchaToken,
|
||||
userSymmetricKey = registerKeyResponse.encryptedUserKey,
|
||||
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
|
||||
publicKey = registerKeyResponse.keys.public,
|
||||
@ -972,18 +954,9 @@ class AuthRepositoryImpl(
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
is RegisterResponseJson.CaptchaRequired -> {
|
||||
it.validationErrors.captchaKeys.firstOrNull()
|
||||
?.let { key -> RegisterResult.CaptchaRequired(captchaId = key) }
|
||||
?: RegisterResult.Error(
|
||||
errorMessage = null,
|
||||
error = MissingPropertyException("Captcha ID"),
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Success -> {
|
||||
settingsRepository.hasUserLoggedInOrCreatedAccount = true
|
||||
RegisterResult.Success(captchaToken = it.captchaBypassToken)
|
||||
RegisterResult.Success
|
||||
}
|
||||
|
||||
is RegisterResponseJson.Invalid -> {
|
||||
@ -1229,10 +1202,6 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
|
||||
captchaTokenChannel.trySend(tokenResult)
|
||||
}
|
||||
|
||||
override fun setDuoCallbackTokenResult(tokenResult: DuoCallbackTokenResult) {
|
||||
duoTokenChannel.trySend(tokenResult)
|
||||
}
|
||||
@ -1624,7 +1593,6 @@ class AuthRepositoryImpl(
|
||||
twoFactorData: TwoFactorDataModel? = null,
|
||||
deviceData: DeviceDataModel? = null,
|
||||
orgIdentifier: String? = null,
|
||||
captchaToken: String?,
|
||||
newDeviceOtp: String? = null,
|
||||
): LoginResult = identityService
|
||||
.getToken(
|
||||
@ -1632,7 +1600,6 @@ class AuthRepositoryImpl(
|
||||
email = email,
|
||||
authModel = authModel,
|
||||
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
|
||||
captchaToken = captchaToken,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
)
|
||||
.fold(
|
||||
@ -1651,10 +1618,6 @@ class AuthRepositoryImpl(
|
||||
},
|
||||
onSuccess = { loginResponse ->
|
||||
when (loginResponse) {
|
||||
is GetTokenResponseJson.CaptchaRequired -> LoginResult.CaptchaRequired(
|
||||
captchaId = loginResponse.captchaKey,
|
||||
)
|
||||
|
||||
is GetTokenResponseJson.TwoFactorRequired -> handleLoginCommonTwoFactorRequired(
|
||||
loginResponse = loginResponse,
|
||||
email = email,
|
||||
|
||||
@ -9,11 +9,6 @@ sealed class LoginResult {
|
||||
*/
|
||||
data object Success : LoginResult()
|
||||
|
||||
/**
|
||||
* Captcha verification is required.
|
||||
*/
|
||||
data class CaptchaRequired(val captchaId: String) : LoginResult()
|
||||
|
||||
/**
|
||||
* Encryption key migration is required.
|
||||
*/
|
||||
|
||||
@ -7,16 +7,8 @@ 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()
|
||||
data object Success : RegisterResult()
|
||||
|
||||
/**
|
||||
* There was an error logging in.
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import java.net.URLEncoder
|
||||
import java.util.Base64
|
||||
import java.util.Locale
|
||||
|
||||
private const val CAPTCHA_HOST: String = "captcha-callback"
|
||||
private const val CALLBACK_URI = "bitwarden://$CAPTCHA_HOST"
|
||||
|
||||
/**
|
||||
* Generates a [Uri] to display a CAPTCHA challenge for Bitwarden authentication.
|
||||
*/
|
||||
fun generateUriForCaptcha(captchaId: String): Uri {
|
||||
val json = buildJsonObject {
|
||||
put(key = "siteKey", value = captchaId)
|
||||
put(key = "locale", value = Locale.getDefault().toString())
|
||||
put(key = "callbackUri", value = CALLBACK_URI)
|
||||
put(key = "captchaRequiredText", value = "Captcha required")
|
||||
}
|
||||
val base64Data = Base64
|
||||
.getEncoder()
|
||||
.encodeToString(
|
||||
json
|
||||
.toString()
|
||||
.toByteArray(Charsets.UTF_8),
|
||||
)
|
||||
val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
|
||||
val url = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||
"?data=$base64Data&parent=$parentParam&v=1"
|
||||
return Uri.parse(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a [CaptchaCallbackTokenResult] from an Intent. There are three possible cases.
|
||||
*
|
||||
* - `null`: Intent is not a captcha callback, or data is null.
|
||||
*
|
||||
* - [CaptchaCallbackTokenResult.MissingToken]:
|
||||
* Intent is the captcha callback, but its missing a token value.
|
||||
*
|
||||
* - [CaptchaCallbackTokenResult.Success]:
|
||||
* Intent is the captcha callback, and it has a token.
|
||||
*/
|
||||
fun Intent.getCaptchaCallbackTokenResult(): CaptchaCallbackTokenResult? {
|
||||
val localData = data
|
||||
return if (
|
||||
action == Intent.ACTION_VIEW && localData != null && localData.host == CAPTCHA_HOST
|
||||
) {
|
||||
localData.getQueryParameter("token")?.let {
|
||||
CaptchaCallbackTokenResult.Success(token = it)
|
||||
} ?: CaptchaCallbackTokenResult.MissingToken
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed class representing the result of captcha callback token extraction.
|
||||
*/
|
||||
sealed class CaptchaCallbackTokenResult {
|
||||
/**
|
||||
* Represents a missing token in the captcha callback.
|
||||
*/
|
||||
data object MissingToken : CaptchaCallbackTokenResult()
|
||||
|
||||
/**
|
||||
* Represents a token present in the captcha callback.
|
||||
*/
|
||||
data class Success(val token: String) : CaptchaCallbackTokenResult()
|
||||
}
|
||||
@ -84,10 +84,9 @@ fun NavGraphBuilder.authGraph(
|
||||
onNavigateToPreventAccountLockout = {
|
||||
navController.navigateToPreventAccountLockout()
|
||||
},
|
||||
onNavigateToLogin = { emailAddress, captchaToken ->
|
||||
onNavigateToLogin = { emailAddress ->
|
||||
navController.navigateToLogin(
|
||||
emailAddress = emailAddress,
|
||||
captchaToken = captchaToken,
|
||||
navOptions = navOptions {
|
||||
popUpTo(route = LandingRoute)
|
||||
},
|
||||
@ -110,7 +109,6 @@ fun NavGraphBuilder.authGraph(
|
||||
onNavigateToLogin = { emailAddress ->
|
||||
navController.navigateToLogin(
|
||||
emailAddress = emailAddress,
|
||||
captchaToken = null,
|
||||
)
|
||||
},
|
||||
onNavigateToEnvironment = {
|
||||
|
||||
@ -65,7 +65,7 @@ fun NavGraphBuilder.completeRegistrationDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToPasswordGuidance: () -> Unit,
|
||||
onNavigateToPreventAccountLockout: () -> Unit,
|
||||
onNavigateToLogin: (email: String, token: String?) -> Unit,
|
||||
onNavigateToLogin: (email: String) -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions<CompleteRegistrationRoute> {
|
||||
CompleteRegistrationScreen(
|
||||
|
||||
@ -69,7 +69,7 @@ fun CompleteRegistrationScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToPasswordGuidance: () -> Unit,
|
||||
onNavigateToPreventAccountLockout: () -> Unit,
|
||||
onNavigateToLogin: (email: String, token: String?) -> Unit,
|
||||
onNavigateToLogin: (email: String) -> Unit,
|
||||
viewModel: CompleteRegistrationViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
@ -94,7 +94,6 @@ fun CompleteRegistrationScreen(
|
||||
is CompleteRegistrationEvent.NavigateToLogin -> {
|
||||
onNavigateToLogin(
|
||||
event.email,
|
||||
event.captchaToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,13 +177,6 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
action: Internal.ReceiveRegisterResult,
|
||||
) {
|
||||
when (val registerAccountResult = action.registerResult) {
|
||||
// TODO PM-6675: Remove captcha from RegisterResult when old flow gets removed
|
||||
is RegisterResult.CaptchaRequired -> {
|
||||
throw IllegalStateException(
|
||||
"Captcha should not be required for the new registration flow",
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
@ -202,12 +195,10 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
val loginResult = authRepository.login(
|
||||
email = state.userEmail,
|
||||
password = state.passwordInput,
|
||||
captchaToken = registerAccountResult.captchaToken,
|
||||
)
|
||||
sendAction(
|
||||
Internal.ReceiveLoginResult(
|
||||
loginResult = loginResult,
|
||||
captchaToken = registerAccountResult.captchaToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -266,7 +257,6 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
sendEvent(
|
||||
CompleteRegistrationEvent.NavigateToLogin(
|
||||
email = state.userEmail,
|
||||
captchaToken = action.captchaToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -390,7 +380,6 @@ class CompleteRegistrationViewModel @Inject constructor(
|
||||
email = state.userEmail,
|
||||
masterPassword = state.passwordInput,
|
||||
masterPasswordHint = state.passwordHintInput.ifBlank { null },
|
||||
captchaToken = null,
|
||||
)
|
||||
sendAction(
|
||||
Internal.ReceiveRegisterResult(
|
||||
@ -527,11 +516,10 @@ sealed class CompleteRegistrationEvent {
|
||||
data object NavigateToMakePasswordStrong : CompleteRegistrationEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the captcha verification screen.
|
||||
* Navigates to the Login screen.
|
||||
*/
|
||||
data class NavigateToLogin(
|
||||
val email: String,
|
||||
val captchaToken: String?,
|
||||
) : CompleteRegistrationEvent()
|
||||
}
|
||||
|
||||
@ -615,14 +603,11 @@ sealed class CompleteRegistrationAction {
|
||||
|
||||
/**
|
||||
* Indicates registration was successful and will now attempt to login and unlock the vault.
|
||||
* @property captchaToken The captcha token to use for login. With the login function this
|
||||
* is possible to be negative.
|
||||
*
|
||||
* @see [AuthRepository.login]
|
||||
*/
|
||||
data class ReceiveLoginResult(
|
||||
val loginResult: LoginResult,
|
||||
val captchaToken: String?,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,10 +64,6 @@ fun EnterpriseSignOnScreen(
|
||||
intentManager.startCustomTabsActivity(event.uri)
|
||||
}
|
||||
|
||||
is EnterpriseSignOnEvent.NavigateToCaptcha -> {
|
||||
intentManager.startCustomTabsActivity(event.uri)
|
||||
}
|
||||
|
||||
is EnterpriseSignOnEvent.NavigateToSetPassword -> {
|
||||
onNavigateToSetPassword()
|
||||
}
|
||||
|
||||
@ -14,10 +14,8 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SSO_URI
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
@ -53,7 +51,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
?: EnterpriseSignOnState(
|
||||
dialogState = null,
|
||||
orgIdentifierInput = "",
|
||||
captchaToken = null,
|
||||
),
|
||||
) {
|
||||
|
||||
@ -80,16 +77,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// Automatically attempt to login again if a captcha token is received.
|
||||
authRepository
|
||||
.captchaTokenResultFlow
|
||||
.onEach {
|
||||
sendAction(
|
||||
EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken(it),
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
checkOrganizationDomainSsoDetails()
|
||||
}
|
||||
|
||||
@ -118,10 +105,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
handleOnLoginResult(action)
|
||||
}
|
||||
|
||||
is EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken -> {
|
||||
handleOnReceiveCaptchaToken(action)
|
||||
}
|
||||
|
||||
is EnterpriseSignOnAction.Internal.OnVerifiedOrganizationDomainSsoDetailsReceive -> {
|
||||
handleOnVerifiedOrganizationDomainSsoDetailsReceive(action)
|
||||
}
|
||||
@ -151,15 +134,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
@Suppress("LongMethod")
|
||||
private fun handleOnLoginResult(action: EnterpriseSignOnAction.Internal.OnLoginResult) {
|
||||
when (val loginResult = action.loginResult) {
|
||||
is LoginResult.CaptchaRequired -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
sendEvent(
|
||||
event = EnterpriseSignOnEvent.NavigateToCaptcha(
|
||||
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is LoginResult.Error -> {
|
||||
showError(
|
||||
message = loginResult.errorMessage?.asText()
|
||||
@ -304,30 +278,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
attemptLogin()
|
||||
}
|
||||
|
||||
private fun handleOnReceiveCaptchaToken(
|
||||
action: EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken,
|
||||
) {
|
||||
when (val tokenResult = action.tokenResult) {
|
||||
CaptchaCallbackTokenResult.MissingToken -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
title = BitwardenString.log_in_denied.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is CaptchaCallbackTokenResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(captchaToken = tokenResult.token)
|
||||
}
|
||||
attemptLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prevalidateSso() {
|
||||
if (!networkConnectionManager.isNetworkConnected) {
|
||||
mutableStateFlow.update {
|
||||
@ -393,7 +343,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
ssoCode = ssoCallbackResult.code,
|
||||
ssoCodeVerifier = ssoData.codeVerifier,
|
||||
ssoRedirectUri = SSO_URI,
|
||||
captchaToken = state.captchaToken,
|
||||
organizationIdentifier = state.orgIdentifierInput,
|
||||
)
|
||||
sendAction(EnterpriseSignOnAction.Internal.OnLoginResult(result))
|
||||
@ -511,7 +460,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
data class EnterpriseSignOnState(
|
||||
val dialogState: DialogState?,
|
||||
val orgIdentifierInput: String,
|
||||
val captchaToken: String?,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
@ -560,11 +508,6 @@ sealed class EnterpriseSignOnEvent {
|
||||
*/
|
||||
data class NavigateToSsoLogin(val uri: Uri) : EnterpriseSignOnEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the captcha verification screen.
|
||||
*/
|
||||
data class NavigateToCaptcha(val uri: Uri) : EnterpriseSignOnEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the set master password screen.
|
||||
*/
|
||||
@ -644,11 +587,6 @@ sealed class EnterpriseSignOnAction {
|
||||
val error: Throwable?,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* A captcha callback result has been received
|
||||
*/
|
||||
data class OnReceiveCaptchaToken(val tokenResult: CaptchaCallbackTokenResult) : Internal()
|
||||
|
||||
/**
|
||||
* A result was received when requesting an [VerifiedOrganizationDomainSsoDetailsResult].
|
||||
*/
|
||||
|
||||
@ -14,13 +14,12 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class LoginRoute(
|
||||
val emailAddress: String,
|
||||
val captchaToken: String?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Class to retrieve login arguments from the [SavedStateHandle].
|
||||
*/
|
||||
data class LoginArgs(val emailAddress: String, val captchaToken: String?)
|
||||
data class LoginArgs(val emailAddress: String)
|
||||
|
||||
/**
|
||||
* Constructs a [LoginArgs] from the [SavedStateHandle] and internal route data.
|
||||
@ -29,7 +28,6 @@ fun SavedStateHandle.toLoginArgs(): LoginArgs {
|
||||
val route = this.toRoute<LoginRoute>()
|
||||
return LoginArgs(
|
||||
emailAddress = route.emailAddress,
|
||||
captchaToken = route.captchaToken,
|
||||
)
|
||||
}
|
||||
|
||||
@ -38,11 +36,10 @@ fun SavedStateHandle.toLoginArgs(): LoginArgs {
|
||||
*/
|
||||
fun NavController.navigateToLogin(
|
||||
emailAddress: String,
|
||||
captchaToken: String?,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate(
|
||||
route = LoginRoute(emailAddress = emailAddress, captchaToken = captchaToken),
|
||||
route = LoginRoute(emailAddress = emailAddress),
|
||||
navOptions = navOptions,
|
||||
)
|
||||
}
|
||||
|
||||
@ -50,8 +50,6 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@ -68,7 +66,6 @@ fun LoginScreen(
|
||||
onNavigateToLoginWithDevice: (emailAddress: String) -> Unit,
|
||||
onNavigateToTwoFactorLogin: (String, String?, Boolean) -> Unit,
|
||||
viewModel: LoginViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
@ -79,10 +76,6 @@ fun LoginScreen(
|
||||
onNavigateToMasterPasswordHint(event.emailAddress)
|
||||
}
|
||||
|
||||
is LoginEvent.NavigateToCaptcha -> {
|
||||
intentManager.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
|
||||
is LoginEvent.NavigateToEnterpriseSignOn -> {
|
||||
onNavigateToEnterpriseSignOn(event.emailAddress)
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@ -15,16 +14,12 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.toUriOrNull
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
|
||||
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.IgnoredOnParcel
|
||||
@ -53,7 +48,6 @@ class LoginViewModel @Inject constructor(
|
||||
passwordInput = "",
|
||||
environmentLabel = environmentRepository.environment.label,
|
||||
dialogState = LoginState.DialogState.Loading(BitwardenString.loading.asText()),
|
||||
captchaToken = args.captchaToken,
|
||||
accountSummaries = authRepository
|
||||
.userStateFlow
|
||||
.value
|
||||
@ -65,16 +59,6 @@ class LoginViewModel @Inject constructor(
|
||||
) {
|
||||
|
||||
init {
|
||||
authRepository.captchaTokenResultFlow
|
||||
.onEach {
|
||||
sendAction(
|
||||
LoginAction.Internal.ReceiveCaptchaToken(
|
||||
tokenResult = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
LoginAction.Internal.ReceiveKnownDeviceResult(
|
||||
@ -98,9 +82,6 @@ class LoginViewModel @Inject constructor(
|
||||
LoginAction.SingleSignOnClick -> handleSingleSignOnClicked()
|
||||
is LoginAction.PasswordInputChanged -> handlePasswordInputChanged(action)
|
||||
is LoginAction.ErrorDialogDismiss -> handleErrorDialogDismiss()
|
||||
is LoginAction.Internal.ReceiveCaptchaToken -> {
|
||||
handleCaptchaTokenReceived(action.tokenResult)
|
||||
}
|
||||
|
||||
is LoginAction.Internal.ReceiveLoginResult -> {
|
||||
handleReceiveLoginResult(action = action)
|
||||
@ -159,15 +140,6 @@ class LoginViewModel @Inject constructor(
|
||||
@Suppress("MaxLineLength", "LongMethod")
|
||||
private fun handleReceiveLoginResult(action: LoginAction.Internal.ReceiveLoginResult) {
|
||||
when (val loginResult = action.loginResult) {
|
||||
is LoginResult.CaptchaRequired -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
sendEvent(
|
||||
event = LoginEvent.NavigateToCaptcha(
|
||||
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is LoginResult.EncryptionKeyMigrationRequired -> {
|
||||
val vaultUrl =
|
||||
environmentRepository
|
||||
@ -256,28 +228,6 @@ class LoginViewModel @Inject constructor(
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
}
|
||||
|
||||
private fun handleCaptchaTokenReceived(tokenResult: CaptchaCallbackTokenResult) {
|
||||
when (tokenResult) {
|
||||
CaptchaCallbackTokenResult.MissingToken -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginState.DialogState.Error(
|
||||
title = BitwardenString.log_in_denied.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is CaptchaCallbackTokenResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(captchaToken = tokenResult.token)
|
||||
}
|
||||
attemptLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseButtonClicked() {
|
||||
sendEvent(LoginEvent.NavigateBack)
|
||||
}
|
||||
@ -302,7 +252,6 @@ class LoginViewModel @Inject constructor(
|
||||
val result = authRepository.login(
|
||||
email = state.emailAddress,
|
||||
password = state.passwordInput,
|
||||
captchaToken = state.captchaToken,
|
||||
)
|
||||
sendAction(
|
||||
LoginAction.Internal.ReceiveLoginResult(
|
||||
@ -344,7 +293,6 @@ data class LoginState(
|
||||
// We never want this saved since the input is sensitive data.
|
||||
@IgnoredOnParcel val passwordInput: String = "",
|
||||
val emailAddress: String,
|
||||
val captchaToken: String?,
|
||||
val environmentLabel: String,
|
||||
val isLoginButtonEnabled: Boolean,
|
||||
val dialogState: DialogState?,
|
||||
@ -391,11 +339,6 @@ sealed class LoginEvent {
|
||||
val emailAddress: String,
|
||||
) : LoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the captcha verification screen.
|
||||
*/
|
||||
data class NavigateToCaptcha(val uri: Uri) : LoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the enterprise single sign on screen.
|
||||
*/
|
||||
@ -496,12 +439,6 @@ sealed class LoginAction {
|
||||
* Models actions that the [LoginViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : LoginAction() {
|
||||
/**
|
||||
* Indicates a captcha callback token has been received.
|
||||
*/
|
||||
data class ReceiveCaptchaToken(
|
||||
val tokenResult: CaptchaCallbackTokenResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a [KnownDeviceResult] has been received and state should be updated.
|
||||
|
||||
@ -41,8 +41,6 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
|
||||
/**
|
||||
* The top level composable for the Login with Device screen.
|
||||
@ -53,15 +51,11 @@ fun LoginWithDeviceScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToTwoFactorLogin: (emailAddress: String) -> Unit,
|
||||
viewModel: LoginWithDeviceViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
LoginWithDeviceEvent.NavigateBack -> onNavigateBack()
|
||||
is LoginWithDeviceEvent.NavigateToCaptcha -> {
|
||||
intentManager.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
|
||||
is LoginWithDeviceEvent.NavigateToTwoFactorLogin -> {
|
||||
onNavigateToTwoFactorLogin(event.emailAddress)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@ -11,8 +10,6 @@ import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
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.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.util.toAuthRequestType
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
|
||||
@ -56,11 +53,6 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
sendNewAuthRequest(isResend = false)
|
||||
authRepository
|
||||
.captchaTokenResultFlow
|
||||
.map { LoginWithDeviceAction.Internal.ReceiveCaptchaToken(tokenResult = it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: LoginWithDeviceAction) {
|
||||
@ -95,10 +87,6 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
handleNewAuthRequestResultReceived(action)
|
||||
}
|
||||
|
||||
is LoginWithDeviceAction.Internal.ReceiveCaptchaToken -> {
|
||||
handleReceiveCaptchaToken(action)
|
||||
}
|
||||
|
||||
is LoginWithDeviceAction.Internal.ReceiveLoginResult -> {
|
||||
handleReceiveLoginResult(action)
|
||||
}
|
||||
@ -125,7 +113,6 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
masterPasswordHash = result.authRequest.masterPasswordHash,
|
||||
asymmetricalKey = requireNotNull(result.authRequest.key),
|
||||
privateKey = result.privateKey,
|
||||
captchaToken = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -183,44 +170,11 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReceiveCaptchaToken(
|
||||
action: LoginWithDeviceAction.Internal.ReceiveCaptchaToken,
|
||||
) {
|
||||
when (val tokenResult = action.tokenResult) {
|
||||
CaptchaCallbackTokenResult.MissingToken -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = BitwardenString.log_in_denied.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is CaptchaCallbackTokenResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(loginData = it.loginData?.copy(captchaToken = tokenResult.token))
|
||||
}
|
||||
attemptLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength", "LongMethod")
|
||||
private fun handleReceiveLoginResult(
|
||||
action: LoginWithDeviceAction.Internal.ReceiveLoginResult,
|
||||
) {
|
||||
when (val loginResult = action.loginResult) {
|
||||
is LoginResult.CaptchaRequired -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
sendEvent(
|
||||
event = LoginWithDeviceEvent.NavigateToCaptcha(
|
||||
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is LoginResult.TwoFactorRequired -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
sendEvent(
|
||||
@ -315,7 +269,6 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
asymmetricalKey = loginData.asymmetricalKey,
|
||||
requestPrivateKey = loginData.privateKey,
|
||||
masterPasswordHash = loginData.masterPasswordHash,
|
||||
captchaToken = loginData.captchaToken,
|
||||
)
|
||||
}
|
||||
|
||||
@ -502,7 +455,6 @@ data class LoginWithDeviceState(
|
||||
data class LoginData(
|
||||
val accessCode: String,
|
||||
val requestId: String,
|
||||
val captchaToken: String?,
|
||||
val masterPasswordHash: String?,
|
||||
val asymmetricalKey: String,
|
||||
val privateKey: String,
|
||||
@ -518,11 +470,6 @@ sealed class LoginWithDeviceEvent {
|
||||
*/
|
||||
data object NavigateBack : LoginWithDeviceEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the captcha verification screen.
|
||||
*/
|
||||
data class NavigateToCaptcha(val uri: Uri) : LoginWithDeviceEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the two-factor login screen.
|
||||
*/
|
||||
@ -566,13 +513,6 @@ sealed class LoginWithDeviceAction {
|
||||
val result: CreateAuthRequestResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a captcha callback token has been received.
|
||||
*/
|
||||
data class ReceiveCaptchaToken(
|
||||
val tokenResult: CaptchaCallbackTokenResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a login result has been received.
|
||||
*/
|
||||
|
||||
@ -104,10 +104,6 @@ fun TwoFactorLoginScreen(
|
||||
intentManager.launchUri(uri = event.uri)
|
||||
}
|
||||
|
||||
is TwoFactorLoginEvent.NavigateToCaptcha -> {
|
||||
intentManager.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
|
||||
is TwoFactorLoginEvent.NavigateToDuo -> {
|
||||
intentManager.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
@ -352,7 +348,6 @@ private fun TwoFactorLoginScreenContentPreview() {
|
||||
displayEmail = "email@dot.com",
|
||||
isContinueButtonEnabled = true,
|
||||
isRememberEnabled = true,
|
||||
captchaToken = null,
|
||||
email = "",
|
||||
password = "",
|
||||
orgIdentifier = null,
|
||||
|
||||
@ -20,10 +20,8 @@ import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForWebAuth
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
@ -71,7 +69,6 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
.preferredAuthMethod
|
||||
.isContinueButtonEnabled,
|
||||
isRememberEnabled = false,
|
||||
captchaToken = null,
|
||||
email = args.emailAddress,
|
||||
password = args.password,
|
||||
orgIdentifier = args.orgIdentifier,
|
||||
@ -95,13 +92,6 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// Automatically attempt to login again if a captcha token is received.
|
||||
authRepository
|
||||
.captchaTokenResultFlow
|
||||
.map { TwoFactorLoginAction.Internal.ReceiveCaptchaToken(tokenResult = it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// Process the Duo result when it is received.
|
||||
authRepository
|
||||
.duoTokenResultFlow
|
||||
@ -147,9 +137,6 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
private fun handleInternalAction(action: TwoFactorLoginAction.Internal) {
|
||||
when (action) {
|
||||
is TwoFactorLoginAction.Internal.ReceiveLoginResult -> handleReceiveLoginResult(action)
|
||||
is TwoFactorLoginAction.Internal.ReceiveCaptchaToken -> {
|
||||
handleCaptchaTokenReceived(action)
|
||||
}
|
||||
|
||||
is TwoFactorLoginAction.Internal.ReceiveDuoResult -> {
|
||||
handleReceiveDuoResult(action)
|
||||
@ -173,30 +160,6 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCaptchaTokenReceived(
|
||||
action: TwoFactorLoginAction.Internal.ReceiveCaptchaToken,
|
||||
) {
|
||||
when (val tokenResult = action.tokenResult) {
|
||||
CaptchaCallbackTokenResult.MissingToken -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = TwoFactorLoginState.DialogState.Error(
|
||||
title = BitwardenString.log_in_denied.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is CaptchaCallbackTokenResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(captchaToken = tokenResult.token)
|
||||
}
|
||||
initiateLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state with the new text and enable or disable the continue button.
|
||||
*/
|
||||
@ -298,14 +261,6 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
|
||||
when (val loginResult = action.loginResult) {
|
||||
// Launch the captcha flow if necessary.
|
||||
is LoginResult.CaptchaRequired -> {
|
||||
sendEvent(
|
||||
event = TwoFactorLoginEvent.NavigateToCaptcha(
|
||||
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// NO-OP: This error shouldn't be possible at this stage.
|
||||
is LoginResult.TwoFactorRequired -> Unit
|
||||
@ -611,7 +566,6 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
email = state.email,
|
||||
password = state.password,
|
||||
newDeviceOtp = code,
|
||||
captchaToken = state.captchaToken,
|
||||
orgIdentifier = state.orgIdentifier,
|
||||
)
|
||||
} else {
|
||||
@ -623,7 +577,6 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
method = state.authMethod.value.toString(),
|
||||
remember = state.isRememberEnabled,
|
||||
),
|
||||
captchaToken = state.captchaToken,
|
||||
orgIdentifier = state.orgIdentifier,
|
||||
)
|
||||
}
|
||||
@ -650,7 +603,6 @@ data class TwoFactorLoginState(
|
||||
val isRememberEnabled: Boolean,
|
||||
val isNewDeviceVerification: Boolean,
|
||||
// Internal
|
||||
val captchaToken: String?,
|
||||
val email: String,
|
||||
val password: String?,
|
||||
val orgIdentifier: String?,
|
||||
@ -711,11 +663,6 @@ sealed class TwoFactorLoginEvent {
|
||||
*/
|
||||
data object NavigateBack : TwoFactorLoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the captcha verification screen.
|
||||
*/
|
||||
data class NavigateToCaptcha(val uri: Uri) : TwoFactorLoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the Duo 2-factor authentication screen.
|
||||
*/
|
||||
@ -804,13 +751,6 @@ sealed class TwoFactorLoginAction {
|
||||
* Models actions that the [TwoFactorLoginViewModel] itself might send.
|
||||
*/
|
||||
sealed class Internal : TwoFactorLoginAction() {
|
||||
/**
|
||||
* Indicates a captcha callback token has been received.
|
||||
*/
|
||||
data class ReceiveCaptchaToken(
|
||||
val tokenResult: CaptchaCallbackTokenResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a Dup callback token has been received.
|
||||
*/
|
||||
|
||||
@ -3,11 +3,9 @@ package com.x8bit.bitwarden
|
||||
import android.content.Intent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getWebAuthResultOrNull
|
||||
@ -26,7 +24,6 @@ import org.junit.jupiter.api.Test
|
||||
|
||||
class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { setCaptchaCallbackTokenResult(any()) } just runs
|
||||
every { setSsoCallbackResult(any()) } just runs
|
||||
every { setDuoCallbackTokenResult(any()) } just runs
|
||||
every { setYubiKeyResult(any()) } just runs
|
||||
@ -38,7 +35,6 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
mockkStatic(
|
||||
Intent::getYubiKeyResultOrNull,
|
||||
Intent::getWebAuthResultOrNull,
|
||||
Intent::getCaptchaCallbackTokenResult,
|
||||
Intent::getDuoCallbackTokenResult,
|
||||
Intent::getSsoCallbackResult,
|
||||
)
|
||||
@ -49,35 +45,16 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
unmockkStatic(
|
||||
Intent::getYubiKeyResultOrNull,
|
||||
Intent::getWebAuthResultOrNull,
|
||||
Intent::getCaptchaCallbackTokenResult,
|
||||
Intent::getDuoCallbackTokenResult,
|
||||
Intent::getSsoCallbackResult,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on IntentReceive with captcha host should call setCaptchaCallbackToken`() {
|
||||
val viewModel = createViewModel()
|
||||
val mockIntent = mockk<Intent>()
|
||||
val captchaCallbackTokenResult = CaptchaCallbackTokenResult.Success(token = "mockk_token")
|
||||
every { mockIntent.getCaptchaCallbackTokenResult() } returns captchaCallbackTokenResult
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns null
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns null
|
||||
every { mockIntent.getWebAuthResultOrNull() } returns null
|
||||
every { mockIntent.getSsoCallbackResult() } returns null
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
verify(exactly = 1) {
|
||||
authRepository.setCaptchaCallbackTokenResult(tokenResult = captchaCallbackTokenResult)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on IntentReceive with duo host should call setDuoCallbackToken`() {
|
||||
val viewModel = createViewModel()
|
||||
val mockIntent = mockk<Intent>()
|
||||
val duoCallbackTokenResult = DuoCallbackTokenResult.Success(token = "mockk_token")
|
||||
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns duoCallbackTokenResult
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns null
|
||||
every { mockIntent.getWebAuthResultOrNull() } returns null
|
||||
@ -100,7 +77,6 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
every { mockIntent.getSsoCallbackResult() } returns sseCallbackResult
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns null
|
||||
every { mockIntent.getWebAuthResultOrNull() } returns null
|
||||
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns null
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
@ -116,7 +92,6 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
val yubiKeyResult = mockk<YubiKeyResult>()
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns yubiKeyResult
|
||||
every { mockIntent.getWebAuthResultOrNull() } returns null
|
||||
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns null
|
||||
every { mockIntent.getSsoCallbackResult() } returns null
|
||||
|
||||
@ -133,7 +108,6 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
||||
val mockIntent = mockk<Intent> {
|
||||
every { getWebAuthResultOrNull() } returns webAuthResult
|
||||
every { getYubiKeyResultOrNull() } returns null
|
||||
every { getCaptchaCallbackTokenResult() } returns null
|
||||
every { getDuoCallbackTokenResult() } returns null
|
||||
every { getSsoCallbackResult() } returns null
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,66 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class CaptchaUtilsTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `generateIntentForCaptcha should return valid Uri`() {
|
||||
val actualUri = generateUriForCaptcha(captchaId = "testCaptchaId")
|
||||
val expectedUrl = "https://vault.bitwarden.com/captcha-mobile-connector.html" +
|
||||
"?data=eyJzaXRlS2V5IjoidGVzdENhcHRjaGFJZCIsImxvY2FsZSI6ImVuX1VTIiwiY2Fsb" +
|
||||
"GJhY2tVcmkiOiJiaXR3YXJkZW46Ly9jYXB0Y2hhLWNhbGxiYWNrIiwiY2FwdGNoYVJlcXVp" +
|
||||
"cmVkVGV4dCI6IkNhcHRjaGEgcmVxdWlyZWQifQ==&parent=bitwarden%3A%2F%2F" +
|
||||
"captcha-callback&v=1"
|
||||
val expectedUri = Uri.parse(expectedUrl)
|
||||
assertEquals(expectedUri, actualUri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCaptchaCallbackToken should return null when data is null`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { data } returns null
|
||||
every { action } returns Intent.ACTION_VIEW
|
||||
}
|
||||
val result = intent.getCaptchaCallbackTokenResult()
|
||||
assertEquals(null, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCaptchaCallbackToken should return null when action is not Intent ACTION_VIEW`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { data } returns null
|
||||
every { action } returns Intent.ACTION_ANSWER
|
||||
}
|
||||
val result = intent.getCaptchaCallbackTokenResult()
|
||||
assertEquals(null, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCaptchaCallbackToken should return MissingToken with missing token parameter`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { data?.getQueryParameter("token") } returns null
|
||||
every { action } returns Intent.ACTION_VIEW
|
||||
every { data?.host } returns "captcha-callback"
|
||||
}
|
||||
val result = intent.getCaptchaCallbackTokenResult()
|
||||
assertEquals(CaptchaCallbackTokenResult.MissingToken, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCaptchaCallbackToken should return Success when token query parameter is present`() {
|
||||
val intent = mockk<Intent> {
|
||||
every { data?.getQueryParameter("token") } returns "myToken"
|
||||
every { action } returns Intent.ACTION_VIEW
|
||||
every { data?.host } returns "captcha-callback"
|
||||
}
|
||||
val result = intent.getCaptchaCallbackTokenResult()
|
||||
assertEquals(CaptchaCallbackTokenResult.Success("myToken"), result)
|
||||
}
|
||||
}
|
||||
@ -61,10 +61,9 @@ class CompleteRegistrationScreenTest : BitwardenComposeTest() {
|
||||
onNavigateToPreventAccountLockout = {
|
||||
onNavigateToPreventAccountLockoutCalled = true
|
||||
},
|
||||
onNavigateToLogin = { email, captchaToken ->
|
||||
onNavigateToLogin = { email ->
|
||||
onNavigateToLoginCalled = true
|
||||
assertTrue(email == EMAIL)
|
||||
assertTrue(captchaToken == TOKEN)
|
||||
},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
@ -261,7 +260,6 @@ class CompleteRegistrationScreenTest : BitwardenComposeTest() {
|
||||
mutableEventFlow.tryEmit(
|
||||
CompleteRegistrationEvent.NavigateToLogin(
|
||||
email = EMAIL,
|
||||
captchaToken = TOKEN,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -18,7 +18,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance.RegistrationEvent
|
||||
@ -63,7 +62,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
login(
|
||||
email = any(),
|
||||
password = any(),
|
||||
captchaToken = any(),
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
|
||||
@ -73,11 +71,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
masterPassword = any(),
|
||||
masterPasswordHint = any(),
|
||||
emailVerificationToken = any(),
|
||||
captchaToken = any(),
|
||||
shouldCheckDataBreaches = any(),
|
||||
isMasterPasswordStrong = any(),
|
||||
)
|
||||
} returns RegisterResult.Success(captchaToken = CAPTCHA_BYPASS_TOKEN)
|
||||
} returns RegisterResult.Success
|
||||
|
||||
coEvery {
|
||||
setOnboardingStatus(OnboardingStatus.NOT_STARTED)
|
||||
@ -103,7 +100,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
specialCircumstanceManager.specialCircumstance = mockCompleteRegistrationCircumstance
|
||||
mockkStatic(
|
||||
SavedStateHandle::toCompleteRegistrationArgs,
|
||||
::generateUriForCaptcha,
|
||||
)
|
||||
}
|
||||
|
||||
@ -111,7 +107,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
fun tearDown() {
|
||||
unmockkStatic(
|
||||
SavedStateHandle::toCompleteRegistrationArgs,
|
||||
::generateUriForCaptcha,
|
||||
)
|
||||
}
|
||||
|
||||
@ -153,11 +148,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
emailVerificationToken = TOKEN,
|
||||
captchaToken = null,
|
||||
shouldCheckDataBreaches = false,
|
||||
isMasterPasswordStrong = true,
|
||||
)
|
||||
} returns RegisterResult.Success(captchaToken = CAPTCHA_BYPASS_TOKEN)
|
||||
} returns RegisterResult.Success
|
||||
val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE)
|
||||
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem())
|
||||
@ -186,7 +180,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
emailVerificationToken = TOKEN,
|
||||
captchaToken = null,
|
||||
shouldCheckDataBreaches = false,
|
||||
isMasterPasswordStrong = true,
|
||||
)
|
||||
@ -220,7 +213,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
mockAuthRepository.login(
|
||||
email = EMAIL,
|
||||
password = PASSWORD,
|
||||
captchaToken = CAPTCHA_BYPASS_TOKEN,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -246,7 +238,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
mockAuthRepository.login(
|
||||
email = EMAIL,
|
||||
password = PASSWORD,
|
||||
captchaToken = CAPTCHA_BYPASS_TOKEN,
|
||||
)
|
||||
} returns LoginResult.TwoFactorRequired
|
||||
|
||||
@ -255,7 +246,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
viewModel.eventFlow.test {
|
||||
assertTrue(awaitItem() is CompleteRegistrationEvent.ShowToast)
|
||||
assertEquals(
|
||||
CompleteRegistrationEvent.NavigateToLogin(EMAIL, CAPTCHA_BYPASS_TOKEN),
|
||||
CompleteRegistrationEvent.NavigateToLogin(EMAIL),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
@ -272,7 +263,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
emailVerificationToken = TOKEN,
|
||||
captchaToken = null,
|
||||
shouldCheckDataBreaches = false,
|
||||
isMasterPasswordStrong = true,
|
||||
)
|
||||
@ -285,7 +275,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
emailVerificationToken = TOKEN,
|
||||
captchaToken = null,
|
||||
shouldCheckDataBreaches = false,
|
||||
isMasterPasswordStrong = true,
|
||||
)
|
||||
@ -302,7 +291,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
emailVerificationToken = TOKEN,
|
||||
captchaToken = null,
|
||||
shouldCheckDataBreaches = true,
|
||||
isMasterPasswordStrong = true,
|
||||
)
|
||||
@ -341,7 +329,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
emailVerificationToken = TOKEN,
|
||||
captchaToken = null,
|
||||
shouldCheckDataBreaches = true,
|
||||
isMasterPasswordStrong = false,
|
||||
)
|
||||
@ -374,7 +361,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
masterPassword = PASSWORD,
|
||||
masterPasswordHint = null,
|
||||
emailVerificationToken = TOKEN,
|
||||
captchaToken = null,
|
||||
shouldCheckDataBreaches = true,
|
||||
isMasterPasswordStrong = false,
|
||||
)
|
||||
@ -681,7 +667,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
|
||||
private const val PASSWORD = "longenoughtpassword"
|
||||
private const val EMAIL = "test@test.com"
|
||||
private const val TOKEN = "token"
|
||||
private const val CAPTCHA_BYPASS_TOKEN = "captcha_bypass"
|
||||
private val DEFAULT_STATE = CompleteRegistrationState(
|
||||
userEmail = EMAIL,
|
||||
emailVerificationToken = TOKEN,
|
||||
|
||||
@ -111,15 +111,6 @@ class EnterpriseSignOnScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToCaptcha should call startCustomTabsActivity`() {
|
||||
val captchaUri = Uri.parse("https://captcha.com")
|
||||
mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateToCaptcha(captchaUri))
|
||||
verify(exactly = 1) {
|
||||
intentManager.startCustomTabsActivity(captchaUri)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToSetPassword should call onNavigateToSetPassword`() {
|
||||
mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateToSetPassword)
|
||||
@ -279,7 +270,6 @@ class EnterpriseSignOnScreenTest : BitwardenComposeTest() {
|
||||
private val DEFAULT_STATE = EnterpriseSignOnState(
|
||||
dialogState = null,
|
||||
orgIdentifierInput = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,9 +14,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.NetworkConnection
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.FakeNetworkConnectionManager
|
||||
@ -43,11 +41,8 @@ import org.junit.jupiter.api.Test
|
||||
class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableSsoCallbackResultFlow = bufferedMutableSharedFlow<SsoCallbackResult>()
|
||||
private val mutableCaptchaTokenResultFlow =
|
||||
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { ssoCallbackResultFlow } returns mutableSsoCallbackResultFlow
|
||||
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
|
||||
every { rememberedOrgIdentifier } returns null
|
||||
every { rememberedOrgIdentifier = "Bitwarden" } just runs
|
||||
coEvery {
|
||||
@ -341,7 +336,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
val orgIdentifier = "Bitwarden"
|
||||
val error = Throwable("Fail!")
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
authRepository.login(any(), any(), any(), any(), any())
|
||||
} returns LoginResult.Error(errorMessage = null, error = error)
|
||||
|
||||
val viewModel = createViewModel(
|
||||
@ -397,7 +392,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
@ -409,7 +403,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val orgIdentifier = "Bitwarden"
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
authRepository.login(any(), any(), any(), any(), any())
|
||||
} returns LoginResult.NewDeviceVerification(errorMessage = "new device verification required")
|
||||
|
||||
val viewModel = createViewModel(
|
||||
@ -464,7 +458,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
@ -476,7 +469,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val orgIdentifier = "Bitwarden"
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
authRepository.login(any(), any(), any(), any(), any())
|
||||
} returns LoginResult.EncryptionKeyMigrationRequired
|
||||
|
||||
environmentRepository.environment = Environment.SelfHosted(
|
||||
@ -539,7 +532,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
@ -551,7 +543,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val orgIdentifier = "Bitwarden"
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
authRepository.login(any(), any(), any(), any(), any())
|
||||
} returns LoginResult.EncryptionKeyMigrationRequired
|
||||
|
||||
environmentRepository.environment = Environment.SelfHosted(
|
||||
@ -614,7 +606,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
@ -626,7 +617,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val orgIdentifier = "Bitwarden"
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
authRepository.login(any(), any(), any(), any(), any())
|
||||
} returns LoginResult.EncryptionKeyMigrationRequired
|
||||
|
||||
environmentRepository.environment = Environment.SelfHosted(
|
||||
@ -689,7 +680,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
@ -701,7 +691,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val orgIdentifier = "Bitwarden"
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
authRepository.login(any(), any(), any(), any(), any())
|
||||
} returns LoginResult.CertificateError
|
||||
|
||||
val viewModel = createViewModel(
|
||||
@ -756,7 +746,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
@ -767,8 +756,9 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
fun `ssoCallbackResultFlow Success with same state with login Success should show loading dialog, hide it, and save org identifier`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
authRepository.login(any(), any(), any(), any(), any())
|
||||
} returns LoginResult.Success
|
||||
|
||||
every {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
} returns "Bitwarden"
|
||||
@ -809,7 +799,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = "Bitwarden",
|
||||
)
|
||||
}
|
||||
@ -818,71 +807,12 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ssoCallbackResultFlow Success with same state with login CaptchaRequired should show loading dialog, hide it, and send NavigateToCaptcha event`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
} returns LoginResult.CaptchaRequired("captcha")
|
||||
every {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
} returns "Bitwarden"
|
||||
val uri: Uri = mockk()
|
||||
every {
|
||||
generateUriForCaptcha(captchaId = "captcha")
|
||||
} returns uri
|
||||
|
||||
val initialState = DEFAULT_STATE.copy(orgIdentifierInput = "Bitwarden")
|
||||
val viewModel = createViewModel(
|
||||
initialState = initialState,
|
||||
ssoData = DEFAULT_SSO_DATA,
|
||||
)
|
||||
val ssoCallbackResult = SsoCallbackResult.Success(state = "abc", code = "lmn")
|
||||
|
||||
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
assertEquals(initialState, stateFlow.awaitItem())
|
||||
|
||||
mutableSsoCallbackResultFlow.tryEmit(ssoCallbackResult)
|
||||
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Loading(
|
||||
BitwardenString.logging_in.asText(),
|
||||
),
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
initialState,
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
EnterpriseSignOnEvent.NavigateToCaptcha(uri),
|
||||
eventFlow.awaitItem(),
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.login(
|
||||
email = "test@gmail.com",
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = "Bitwarden",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ssoCallbackResultFlow Success with same state with login TwoFactorRequired should show loading dialog, hide it, and send NavigateToTwoFactorLogin event`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
authRepository.login(any(), any(), any(), any(), any())
|
||||
} returns LoginResult.TwoFactorRequired
|
||||
every {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
@ -925,7 +855,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = "Bitwarden",
|
||||
)
|
||||
}
|
||||
@ -936,7 +865,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val orgIdentifier = "Bitwarden"
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
authRepository.login(any(), any(), any(), any(), any())
|
||||
} returns LoginResult.ConfirmKeyConnectorDomain("bitwarden.com")
|
||||
|
||||
val viewModel = createViewModel(
|
||||
@ -990,75 +919,11 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
ssoCode = "lmn",
|
||||
ssoCodeVerifier = "def",
|
||||
ssoRedirectUri = "bitwarden://sso-callback",
|
||||
captchaToken = null,
|
||||
organizationIdentifier = orgIdentifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `captchaTokenResultFlow MissingToken should show error dialog`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
|
||||
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.MissingToken)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
title = BitwardenString.log_in_denied.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `captchaTokenResultFlow Success should update the state and attempt to login`() = runTest {
|
||||
coEvery {
|
||||
authRepository.login(any(), any(), any(), any(), any(), any())
|
||||
} returns LoginResult.Success
|
||||
every {
|
||||
authRepository.rememberedOrgIdentifier
|
||||
} returns "Bitwarden"
|
||||
|
||||
val initialState = DEFAULT_STATE.copy(orgIdentifierInput = "Bitwarden")
|
||||
val viewModel = createViewModel(
|
||||
initialState = initialState,
|
||||
ssoData = DEFAULT_SSO_DATA,
|
||||
ssoCallbackResult = SsoCallbackResult.Success(
|
||||
state = "abc",
|
||||
code = "lmn",
|
||||
),
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
initialState,
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token"))
|
||||
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
captchaToken = "token",
|
||||
dialogState = EnterpriseSignOnState.DialogState.Loading(
|
||||
BitwardenString.logging_in.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
initialState.copy(captchaToken = "token"),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `OrganizationDomainSsoDetails failure should make a request, hide the dialog, and update the org input based on the remembered org`() =
|
||||
@ -1415,7 +1280,6 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
|
||||
private val DEFAULT_STATE = EnterpriseSignOnState(
|
||||
dialogState = null,
|
||||
orgIdentifierInput = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
private val DEFAULT_SSO_DATA = SsoResponseData(
|
||||
state = "abc",
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.ui.platform.SoftwareKeyboardController
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
@ -318,13 +317,6 @@ class LoginScreenTest : BitwardenComposeTest() {
|
||||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToCaptcha should call intentManager startCustomTabsActivity`() {
|
||||
val mockUri = mockk<Uri>()
|
||||
mutableEventFlow.tryEmit(LoginEvent.NavigateToCaptcha(mockUri))
|
||||
verify { intentManager.startCustomTabsActivity(mockUri) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToMasterPasswordHint should call onNavigateToMasterPasswordHint`() {
|
||||
mutableEventFlow.tryEmit(LoginEvent.NavigateToMasterPasswordHint("email"))
|
||||
@ -359,7 +351,6 @@ private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||
private val DEFAULT_STATE =
|
||||
LoginState(
|
||||
emailAddress = EMAIL,
|
||||
captchaToken = null,
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
environmentLabel = "",
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
@ -15,8 +13,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
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.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@ -41,14 +37,11 @@ import org.junit.jupiter.api.Test
|
||||
@Suppress("LargeClass")
|
||||
class LoginViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableCaptchaTokenResultFlow =
|
||||
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
|
||||
private val authRepository: AuthRepository = mockk(relaxed = true) {
|
||||
coEvery {
|
||||
getIsKnownDevice(EMAIL)
|
||||
} returns KnownDeviceResult.Success(false)
|
||||
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { logout(any()) } just runs
|
||||
}
|
||||
@ -60,7 +53,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(
|
||||
::generateUriForCaptcha,
|
||||
SavedStateHandle::toLoginArgs,
|
||||
)
|
||||
}
|
||||
@ -68,7 +60,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(
|
||||
::generateUriForCaptcha,
|
||||
SavedStateHandle::toLoginArgs,
|
||||
)
|
||||
}
|
||||
@ -265,7 +256,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.Error(errorMessage = "mock_error", error = error)
|
||||
val viewModel = createViewModel()
|
||||
@ -292,7 +282,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,7 +294,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.UnofficialServerError
|
||||
val viewModel = createViewModel()
|
||||
@ -330,7 +319,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,7 +331,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.EncryptionKeyMigrationRequired
|
||||
fakeEnvironmentRepository.environment = Environment.SelfHosted(
|
||||
@ -378,7 +366,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -390,7 +378,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.EncryptionKeyMigrationRequired
|
||||
fakeEnvironmentRepository.environment = Environment.SelfHosted(
|
||||
@ -424,7 +411,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -436,7 +423,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.EncryptionKeyMigrationRequired
|
||||
fakeEnvironmentRepository.environment = Environment.SelfHosted(
|
||||
@ -470,7 +456,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -482,7 +468,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.CertificateError
|
||||
val viewModel = createViewModel()
|
||||
@ -508,7 +493,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,7 +503,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
val viewModel = createViewModel()
|
||||
@ -539,35 +523,10 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LoginButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
|
||||
runTest {
|
||||
val mockkUri = mockk<Uri>()
|
||||
every {
|
||||
generateUriForCaptcha(captchaId = "mock_captcha_id")
|
||||
} returns mockkUri
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LoginAction.LoginButtonClick)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
assertEquals(LoginEvent.NavigateToCaptcha(uri = mockkUri), awaitItem())
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `LoginButtonClick login returns TwoFactorRequired should base64 URL encode password and emit NavigateToTwoFactorLogin`() =
|
||||
@ -577,7 +536,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = password,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.TwoFactorRequired
|
||||
|
||||
@ -604,7 +562,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = password,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -618,7 +575,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = password,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.NewDeviceVerification(errorMessage = "new device verification needed")
|
||||
|
||||
@ -643,7 +599,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = password, captchaToken = null)
|
||||
authRepository.login(email = EMAIL, password = password)
|
||||
}
|
||||
}
|
||||
|
||||
@ -743,22 +699,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `captchaTokenFlow success update should trigger a login`() = runTest {
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
password = "",
|
||||
captchaToken = "token",
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
createViewModel()
|
||||
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token"))
|
||||
coVerify {
|
||||
authRepository.login(email = EMAIL, password = "", captchaToken = "token")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(state: LoginState? = null): LoginViewModel =
|
||||
LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
@ -766,7 +706,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
vaultRepository = vaultRepository,
|
||||
savedStateHandle = SavedStateHandle().apply {
|
||||
set(key = "state", value = state)
|
||||
every { toLoginArgs() } returns LoginArgs(emailAddress = EMAIL, captchaToken = null)
|
||||
every { toLoginArgs() } returns LoginArgs(emailAddress = EMAIL)
|
||||
},
|
||||
)
|
||||
|
||||
@ -778,7 +718,6 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||
isLoginButtonEnabled = false,
|
||||
environmentLabel = Environment.Us.label,
|
||||
dialogState = null,
|
||||
captchaToken = null,
|
||||
accountSummaries = emptyList(),
|
||||
shouldShowLoginWithDevice = false,
|
||||
)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
@ -113,15 +112,6 @@ class LoginWithDeviceScreenTest : BitwardenComposeTest() {
|
||||
assertEquals(email, onNavigateToTwoFactorLoginEmail)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToCaptcha should call launchUri on intentManager`() {
|
||||
val uri = mockk<Uri>()
|
||||
mutableEventFlow.tryEmit(LoginWithDeviceEvent.NavigateToCaptcha(uri))
|
||||
verify(exactly = 1) {
|
||||
intentManager.startCustomTabsActivity(uri)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `progress bar should be displayed according to state`() {
|
||||
mutableStateFlow.update {
|
||||
|
||||
@ -11,12 +11,10 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
|
||||
import io.mockk.awaits
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
@ -38,13 +36,10 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableCreateAuthRequestWithUpdatesFlow =
|
||||
bufferedMutableSharedFlow<CreateAuthRequestResult>()
|
||||
private val mutableCaptchaTokenResultFlow =
|
||||
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
coEvery {
|
||||
createAuthRequestWithUpdates(email = EMAIL, authRequestType = any())
|
||||
} returns mutableCreateAuthRequestWithUpdatesFlow
|
||||
coEvery { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
|
||||
}
|
||||
private val snackbarRelayManager: SnackbarRelayManager = mockk {
|
||||
every { sendSnackbarData(data = any(), relay = any()) } just runs
|
||||
@ -181,7 +176,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
|
||||
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
|
||||
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
val viewModel = createViewModel()
|
||||
@ -232,7 +226,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
|
||||
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
|
||||
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -315,7 +308,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
|
||||
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
|
||||
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.TwoFactorRequired
|
||||
val viewModel = createViewModel()
|
||||
@ -341,7 +333,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
|
||||
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
|
||||
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -358,7 +349,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
|
||||
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
|
||||
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.Error(errorMessage = null, error = error)
|
||||
val viewModel = createViewModel()
|
||||
@ -409,7 +399,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
|
||||
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
|
||||
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -426,7 +415,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
|
||||
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
|
||||
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.UnofficialServerError
|
||||
val viewModel = createViewModel()
|
||||
@ -476,7 +464,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
|
||||
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
|
||||
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -493,7 +480,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
|
||||
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
|
||||
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.CertificateError
|
||||
val viewModel = createViewModel()
|
||||
@ -543,7 +529,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
|
||||
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
|
||||
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -560,7 +545,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
|
||||
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
|
||||
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.NewDeviceVerification(errorMessage = "new device verification required")
|
||||
val viewModel = createViewModel()
|
||||
@ -610,69 +594,10 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
|
||||
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
|
||||
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on captchaTokenResultFlow missing token should should display error dialog`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.MissingToken)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = LoginWithDeviceState.DialogState.Error(
|
||||
title = BitwardenString.log_in_denied.asText(),
|
||||
message = BitwardenString.captcha_failed.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on captchaTokenResultFlow success should update the token`() = runTest {
|
||||
val captchaToken = "captchaToken"
|
||||
val initialState = DEFAULT_STATE.copy(loginData = DEFAULT_LOGIN_DATA)
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
requestId = DEFAULT_LOGIN_DATA.requestId,
|
||||
accessCode = DEFAULT_LOGIN_DATA.accessCode,
|
||||
asymmetricalKey = DEFAULT_LOGIN_DATA.asymmetricalKey,
|
||||
requestPrivateKey = DEFAULT_LOGIN_DATA.privateKey,
|
||||
masterPasswordHash = DEFAULT_LOGIN_DATA.masterPasswordHash,
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
} just awaits
|
||||
val viewModel = createViewModel(initialState)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success(captchaToken))
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
loginData = DEFAULT_LOGIN_DATA.copy(captchaToken = captchaToken),
|
||||
dialogState = LoginWithDeviceState.DialogState.Loading(
|
||||
message = BitwardenString.logging_in.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.login(
|
||||
email = EMAIL,
|
||||
requestId = AUTH_REQUEST.id,
|
||||
accessCode = AUTH_REQUEST_ACCESS_CODE,
|
||||
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
|
||||
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
|
||||
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on createAuthRequestWithUpdates Error received should show content with error dialog`() {
|
||||
val error = Throwable("Fail!")
|
||||
@ -803,5 +728,4 @@ private val DEFAULT_LOGIN_DATA = LoginWithDeviceState.LoginData(
|
||||
masterPasswordHash = "verySecureHash",
|
||||
asymmetricalKey = "public",
|
||||
privateKey = "private_key",
|
||||
captchaToken = null,
|
||||
)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.startregistration
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
@ -10,7 +9,6 @@ import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ContinueClick
|
||||
@ -33,20 +31,13 @@ import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
|
||||
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
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class StartRegistrationViewModelTest : BaseViewModelTest() {
|
||||
private val mockAuthRepository = mockk<AuthRepository> {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
}
|
||||
private val mockAuthRepository = mockk<AuthRepository>()
|
||||
private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>()
|
||||
private val snackbarRelayManager = mockk<SnackbarRelayManager> {
|
||||
every {
|
||||
@ -55,16 +46,6 @@ class StartRegistrationViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(::generateUriForCaptcha)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(::generateUriForCaptcha)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
val viewModel = createViewModel()
|
||||
@ -186,10 +167,6 @@ class StartRegistrationViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `ContinueClick register returns Success with emailVerificationToken should emit NavigateToCompleteRegistration`() =
|
||||
runTest {
|
||||
val mockkUri = mockk<Uri>()
|
||||
every {
|
||||
generateUriForCaptcha(captchaId = "mock_captcha_id")
|
||||
} returns mockkUri
|
||||
coEvery {
|
||||
mockAuthRepository.sendVerificationEmail(
|
||||
email = EMAIL,
|
||||
@ -216,10 +193,6 @@ class StartRegistrationViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `ContinueClick register returns Success without emailVerificationToken should emit NavigateToCheckEmail`() =
|
||||
runTest {
|
||||
val mockkUri = mockk<Uri>()
|
||||
every {
|
||||
generateUriForCaptcha(captchaId = "mock_captcha_id")
|
||||
} returns mockkUri
|
||||
coEvery {
|
||||
mockAuthRepository.sendVerificationEmail(
|
||||
email = EMAIL,
|
||||
|
||||
@ -270,13 +270,6 @@ class TwoFactorLoginScreenTest : BitwardenComposeTest() {
|
||||
TestCase.assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToCaptcha should call intentManager startCustomTabsActivity`() {
|
||||
val mockUri = mockk<Uri>()
|
||||
mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToCaptcha(mockUri))
|
||||
verify { intentManager.startCustomTabsActivity(mockUri) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToDuo should call intentManager startCustomTabsActivity`() {
|
||||
val mockUri = mockk<Uri>()
|
||||
@ -351,7 +344,6 @@ private val DEFAULT_STATE = TwoFactorLoginState(
|
||||
isContinueButtonEnabled = false,
|
||||
isRememberEnabled = false,
|
||||
isNewDeviceVerification = false,
|
||||
captchaToken = null,
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
orgIdentifier = "orgIdentifier",
|
||||
|
||||
@ -16,10 +16,8 @@ import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForWebAuth
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
@ -42,14 +40,11 @@ import org.junit.jupiter.api.Test
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
private val mutableCaptchaTokenResultFlow =
|
||||
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
|
||||
private val mutableDuoTokenResultFlow = bufferedMutableSharedFlow<DuoCallbackTokenResult>()
|
||||
private val mutableYubiKeyResultFlow = bufferedMutableSharedFlow<YubiKeyResult>()
|
||||
private val mutableWebAuthResultFlow = bufferedMutableSharedFlow<WebAuthResult>()
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { twoFactorResponse } returns TWO_FACTOR_RESPONSE
|
||||
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
|
||||
every { duoTokenResultFlow } returns mutableDuoTokenResultFlow
|
||||
every { yubiKeyResultFlow } returns mutableYubiKeyResultFlow
|
||||
every { webAuthResultFlow } returns mutableWebAuthResultFlow
|
||||
@ -59,7 +54,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
email = any(),
|
||||
password = any(),
|
||||
twoFactorData = any(),
|
||||
captchaToken = any(),
|
||||
orgIdentifier = any(),
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
@ -72,7 +66,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(
|
||||
::generateUriForCaptcha,
|
||||
::generateUriForWebAuth,
|
||||
SavedStateHandle::toTwoFactorLoginArgs,
|
||||
)
|
||||
@ -82,7 +75,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(
|
||||
::generateUriForCaptcha,
|
||||
::generateUriForWebAuth,
|
||||
SavedStateHandle::toTwoFactorLoginArgs,
|
||||
)
|
||||
@ -177,7 +169,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.YUBI_KEY.value.toString(),
|
||||
remember = DEFAULT_STATE.isRememberEnabled,
|
||||
),
|
||||
captchaToken = DEFAULT_STATE.captchaToken,
|
||||
orgIdentifier = DEFAULT_STATE.orgIdentifier,
|
||||
)
|
||||
}
|
||||
@ -196,7 +187,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.WEB_AUTH.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
@ -217,7 +207,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.WEB_AUTH.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
@ -254,38 +243,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `captchaTokenFlow success update should trigger a login`() = runTest {
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = DEFAULT_EMAIL_ADDRESS,
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = "token",
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
createViewModel()
|
||||
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token"))
|
||||
coVerify {
|
||||
authRepository.login(
|
||||
email = DEFAULT_EMAIL_ADDRESS,
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = "token",
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `duoTokenResultFlow success update should trigger a login`() = runTest {
|
||||
coEvery {
|
||||
@ -297,7 +254,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.DUO.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
@ -316,7 +272,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.DUO.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
@ -411,7 +366,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
@ -443,7 +397,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
@ -460,7 +413,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
val response = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = authMethodsData,
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -495,7 +447,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
val response = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = authMethodsData,
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -531,7 +482,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
val data = JsonObject(mapOf("AuthUrl" to JsonPrimitive("bitwarden.com")))
|
||||
val response = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = mapOf(TwoFactorAuthMethod.WEB_AUTH to data),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -574,7 +524,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
runTest {
|
||||
val response = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = emptyMap(),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -595,55 +544,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
|
||||
runTest {
|
||||
val mockkUri = mockk<Uri>()
|
||||
every {
|
||||
generateUriForCaptcha(captchaId = "mock_captcha_id")
|
||||
} returns mockkUri
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = DEFAULT_EMAIL_ADDRESS,
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(TwoFactorLoginAction.ContinueButtonClick)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
TwoFactorLoginEvent.NavigateToCaptcha(uri = mockkUri),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(
|
||||
email = DEFAULT_EMAIL_ADDRESS,
|
||||
password = DEFAULT_PASSWORD,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "",
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick login returns Error should update dialogState`() = runTest {
|
||||
val error = Throwable("Fail!")
|
||||
@ -656,7 +556,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.Error(errorMessage = null, error = error)
|
||||
@ -698,7 +597,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
@ -717,7 +615,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.Error(errorMessage = "Mock error message", error = error)
|
||||
@ -759,7 +656,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
@ -778,7 +674,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.UnofficialServerError
|
||||
@ -819,7 +714,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
@ -838,7 +732,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.CertificateError
|
||||
@ -879,7 +772,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
@ -898,7 +790,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
} returns LoginResult.NewDeviceVerification(errorMessage = "new device verification required")
|
||||
@ -939,7 +830,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
method = TwoFactorAuthMethod.AUTHENTICATOR_APP.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
)
|
||||
}
|
||||
@ -1204,7 +1094,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
val localAuthRepository: AuthRepository = mockk {
|
||||
every { twoFactorResponse } returns TWO_FACTOR_RESPONSE
|
||||
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
|
||||
every { duoTokenResultFlow } returns mutableDuoTokenResultFlow
|
||||
every { yubiKeyResultFlow } returns mutableYubiKeyResultFlow
|
||||
every { webAuthResultFlow } returns mutableWebAuthResultFlow
|
||||
@ -1213,7 +1102,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
email = any(),
|
||||
password = any(),
|
||||
newDeviceOtp = any(),
|
||||
captchaToken = any(),
|
||||
orgIdentifier = any(),
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
@ -1243,7 +1131,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
email = DEFAULT_STATE.email,
|
||||
password = DEFAULT_STATE.password,
|
||||
newDeviceOtp = code.trim(),
|
||||
captchaToken = DEFAULT_STATE.captchaToken,
|
||||
orgIdentifier = DEFAULT_STATE.orgIdentifier,
|
||||
)
|
||||
}
|
||||
@ -1325,7 +1212,6 @@ private val TWO_FACTOR_AUTH_METHODS_DATA = mapOf(
|
||||
|
||||
private val TWO_FACTOR_RESPONSE = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = TWO_FACTOR_AUTH_METHODS_DATA,
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -1345,7 +1231,6 @@ private val DEFAULT_STATE = TwoFactorLoginState(
|
||||
dialogState = null,
|
||||
isContinueButtonEnabled = false,
|
||||
isRememberEnabled = false,
|
||||
captchaToken = null,
|
||||
email = DEFAULT_EMAIL_ADDRESS,
|
||||
password = DEFAULT_PASSWORD,
|
||||
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
|
||||
|
||||
@ -39,7 +39,6 @@ internal interface UnauthenticatedIdentityApi {
|
||||
@Field(value = "deviceName") deviceName: String,
|
||||
@Field(value = "deviceType") deviceType: String,
|
||||
@Field(value = "grant_type") grantType: String,
|
||||
@Field(value = "captchaResponse") captchaResponse: String?,
|
||||
@Field(value = "code") ssoCode: String?,
|
||||
@Field(value = "code_verifier") ssoCodeVerifier: String?,
|
||||
@Field(value = "redirect_uri") ssoRedirectUri: String?,
|
||||
|
||||
@ -83,15 +83,6 @@ sealed class GetTokenResponseJson {
|
||||
val keyConnectorUrl: String?,
|
||||
) : GetTokenResponseJson()
|
||||
|
||||
/**
|
||||
* Models json body of a captcha error.
|
||||
*/
|
||||
@Serializable
|
||||
data class CaptchaRequired(
|
||||
@SerialName("HCaptcha_SiteKey")
|
||||
val captchaKey: String,
|
||||
) : GetTokenResponseJson()
|
||||
|
||||
/**
|
||||
* Models json body of an invalid request.
|
||||
*
|
||||
@ -134,11 +125,12 @@ sealed class GetTokenResponseJson {
|
||||
get() = if (errorMessage?.lowercase() == "new device verification required") {
|
||||
InvalidType.NewDeviceVerification
|
||||
} else if (errorMessage
|
||||
?.lowercase()
|
||||
?.contains(
|
||||
"encryption key migration is required. please log in to the web vault at",
|
||||
) == true) {
|
||||
InvalidType.EncryptionKeyMigrationRequired
|
||||
?.lowercase()
|
||||
?.contains(
|
||||
"encryption key migration is required. please log in to the web vault at",
|
||||
) == true
|
||||
) {
|
||||
InvalidType.EncryptionKeyMigrationRequired
|
||||
} else {
|
||||
InvalidType.GenericInvalid
|
||||
}
|
||||
@ -164,9 +156,6 @@ sealed class GetTokenResponseJson {
|
||||
* `{"1":{"Email":"sh*****@example.com"},"0":{"Email":null}}`
|
||||
* The keys are the raw values of the [TwoFactorAuthMethod],
|
||||
* and the map is any extra information for the method.
|
||||
* @property captchaToken The captcha token used in the second
|
||||
* login attempt if the user has already passed a captcha
|
||||
* authentication in the first attempt.
|
||||
* @property ssoToken If the user is logging on via Single
|
||||
* Sign On, they'll need this value to complete authentication
|
||||
* after entering their two-factor code.
|
||||
@ -179,9 +168,6 @@ sealed class GetTokenResponseJson {
|
||||
@SerialName("TwoFactorProviders")
|
||||
val twoFactorProviders: List<String>?,
|
||||
|
||||
@SerialName("CaptchaBypassToken")
|
||||
val captchaToken: String?,
|
||||
|
||||
@SerialName("SsoEmail2faSessionToken")
|
||||
val ssoToken: String?,
|
||||
) : GetTokenResponseJson()
|
||||
|
||||
@ -10,7 +10,6 @@ import kotlinx.serialization.Serializable
|
||||
* @param emailVerificationToken token used to finish the registration process.
|
||||
* @param masterPasswordHash the master password (encrypted).
|
||||
* @param masterPasswordHint the hint for the master password (nullable).
|
||||
* @param captchaResponse the captcha bypass token.
|
||||
* @param userSymmetricKey the user key for the request (encrypted).
|
||||
* @param userAsymmetricKeys a [Keys] object containing public and private keys.
|
||||
* @param kdfType the kdf type represented as an [Int].
|
||||
@ -30,9 +29,6 @@ data class RegisterFinishRequestJson(
|
||||
@SerialName("masterPasswordHint")
|
||||
val masterPasswordHint: String?,
|
||||
|
||||
@SerialName("captchaResponse")
|
||||
val captchaResponse: String?,
|
||||
|
||||
@SerialName("userSymmetricKey")
|
||||
val userSymmetricKey: String,
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ import kotlinx.serialization.Serializable
|
||||
* @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].
|
||||
@ -26,9 +25,6 @@ data class RegisterRequestJson(
|
||||
@SerialName("masterPasswordHint")
|
||||
val masterPasswordHint: String?,
|
||||
|
||||
@SerialName("captchaResponse")
|
||||
val captchaResponse: String?,
|
||||
|
||||
@SerialName("key")
|
||||
val key: String,
|
||||
|
||||
|
||||
@ -13,37 +13,9 @@ 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>,
|
||||
)
|
||||
}
|
||||
data object Success : RegisterResponseJson()
|
||||
|
||||
/**
|
||||
* Represents the json body of an invalid register request.
|
||||
|
||||
@ -36,7 +36,6 @@ interface IdentityService {
|
||||
* @param email user's email address.
|
||||
* @param authModel information necessary to authenticate with any
|
||||
* of the available login methods.
|
||||
* @param captchaToken captcha token to be passed to the API (nullable).
|
||||
* @param twoFactorData the two-factor data, if applicable.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@ -44,7 +43,6 @@ interface IdentityService {
|
||||
uniqueAppId: String,
|
||||
email: String,
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel? = null,
|
||||
newDeviceOtp: String? = null,
|
||||
): Result<GetTokenResponseJson>
|
||||
|
||||
@ -43,11 +43,7 @@ internal class IdentityServiceImpl(
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
|
||||
codes = listOf(
|
||||
NetworkErrorCode.BAD_REQUEST,
|
||||
NetworkErrorCode.TOO_MANY_REQUESTS,
|
||||
@ -61,7 +57,6 @@ internal class IdentityServiceImpl(
|
||||
uniqueAppId: String,
|
||||
email: String,
|
||||
authModel: IdentityTokenAuthModel,
|
||||
captchaToken: String?,
|
||||
twoFactorData: TwoFactorDataModel?,
|
||||
newDeviceOtp: String?,
|
||||
): Result<GetTokenResponseJson> = unauthenticatedIdentityApi
|
||||
@ -81,7 +76,6 @@ internal class IdentityServiceImpl(
|
||||
twoFactorCode = twoFactorData?.code,
|
||||
twoFactorMethod = twoFactorData?.method,
|
||||
twoFactorRemember = twoFactorData?.remember?.let { if (it) "1" else "0 " },
|
||||
captchaResponse = captchaToken,
|
||||
authRequestId = authModel.authRequestId,
|
||||
newDeviceOtp = newDeviceOtp,
|
||||
)
|
||||
@ -89,11 +83,7 @@ internal class IdentityServiceImpl(
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<GetTokenResponseJson.CaptchaRequired>(
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: bitwardenError.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
|
||||
.parseErrorBodyOrNull<GetTokenResponseJson.TwoFactorRequired>(
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
|
||||
@ -134,10 +134,8 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
|
||||
@Test
|
||||
fun `register success json should be Success`() = runTest {
|
||||
val expectedResponse = RegisterResponseJson.Success(
|
||||
captchaBypassToken = "mock_token",
|
||||
)
|
||||
val response = MockResponse().setBody(CAPTCHA_BYPASS_TOKEN_RESPONSE_JSON)
|
||||
val expectedResponse = RegisterResponseJson.Success
|
||||
val response = MockResponse().setBody(LOGIN_SUCCESS_JSON)
|
||||
server.enqueue(response)
|
||||
assertEquals(
|
||||
expectedResponse.asSuccess(),
|
||||
@ -175,30 +173,6 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@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(
|
||||
expectedResponse.asSuccess(),
|
||||
identityService.register(registerRequestBody),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getToken when request response is Success should return Success`() = runTest {
|
||||
server.enqueue(MockResponse().setBody(LOGIN_SUCCESS_JSON))
|
||||
@ -208,7 +182,6 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
assertEquals(LOGIN_SUCCESS.asSuccess(), result)
|
||||
@ -223,27 +196,11 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getToken when response is CaptchaRequired should return CaptchaRequired`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(400).setBody(CAPTCHA_BODY_JSON))
|
||||
val result = identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.MasterPassword(
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
assertEquals(CAPTCHA_BODY.asSuccess(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getToken when response is TwoFactorRequired should return TwoFactorRequired`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(400).setBody(TWO_FACTOR_BODY_JSON))
|
||||
@ -253,7 +210,6 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
assertEquals(TWO_FACTOR_BODY.asSuccess(), result)
|
||||
@ -268,7 +224,6 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
assertEquals(INVALID_LOGIN.asSuccess(), result)
|
||||
@ -284,7 +239,6 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
assertEquals(INVALID_LOGIN.asSuccess(), result)
|
||||
@ -349,10 +303,8 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
|
||||
@Test
|
||||
fun `registerFinish success json should be Success`() = runTest {
|
||||
val expectedResponse = RegisterResponseJson.Success(
|
||||
captchaBypassToken = "mock_token",
|
||||
)
|
||||
val response = MockResponse().setBody(CAPTCHA_BYPASS_TOKEN_RESPONSE_JSON)
|
||||
val expectedResponse = RegisterResponseJson.Success
|
||||
val response = MockResponse().setBody(LOGIN_SUCCESS_JSON)
|
||||
server.enqueue(response)
|
||||
assertEquals(
|
||||
expectedResponse.asSuccess(),
|
||||
@ -494,7 +446,6 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
email = EMAIL,
|
||||
masterPasswordHash = "mockk_masterPasswordHash",
|
||||
masterPasswordHint = "mockk_masterPasswordHint",
|
||||
captchaResponse = "mockk_captchaResponse",
|
||||
key = "mockk_key",
|
||||
keys = RegisterRequestJson.Keys(
|
||||
publicKey = "mockk_publicKey",
|
||||
@ -508,7 +459,6 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
masterPasswordHash = "mockk_masterPasswordHash",
|
||||
masterPasswordHint = "mockk_masterPasswordHint",
|
||||
emailVerificationToken = "mock_emailVerificationToken",
|
||||
captchaResponse = "mockk_captchaResponse",
|
||||
userSymmetricKey = "mockk_key",
|
||||
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
|
||||
publicKey = "mockk_publicKey",
|
||||
@ -566,18 +516,10 @@ private val REFRESH_TOKEN_SUCCESS_BODY = RefreshTokenResponseJson.Success(
|
||||
tokenType = "Bearer",
|
||||
)
|
||||
|
||||
private const val CAPTCHA_BODY_JSON = """
|
||||
{
|
||||
"HCaptcha_SiteKey": "123"
|
||||
}
|
||||
"""
|
||||
private val CAPTCHA_BODY = GetTokenResponseJson.CaptchaRequired("123")
|
||||
|
||||
private const val TWO_FACTOR_BODY_JSON = """
|
||||
{
|
||||
"TwoFactorProviders2": {"1": {"Email": "ex***@email.com"}, "0": {"Email": null}},
|
||||
"SsoEmail2faSessionToken": "exampleToken",
|
||||
"CaptchaBypassToken": "BWCaptchaBypass_ABCXYZ",
|
||||
"TwoFactorProviders": ["1", "3", "0"]
|
||||
}
|
||||
"""
|
||||
@ -587,7 +529,6 @@ private val TWO_FACTOR_BODY = GetTokenResponseJson.TwoFactorRequired(
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
|
||||
),
|
||||
ssoToken = "exampleToken",
|
||||
captchaToken = "BWCaptchaBypass_ABCXYZ",
|
||||
twoFactorProviders = listOf("1", "3", "0"),
|
||||
)
|
||||
|
||||
@ -708,12 +649,6 @@ private const val INVALID_MODEL_STATE_EMAIL_TAKEN_ERROR_JSON = """
|
||||
}
|
||||
"""
|
||||
|
||||
private const val CAPTCHA_BYPASS_TOKEN_RESPONSE_JSON = """
|
||||
{
|
||||
"captchaBypassToken": "mock_token"
|
||||
}
|
||||
"""
|
||||
|
||||
private val INVALID_LOGIN = GetTokenResponseJson.Invalid(
|
||||
errorModel = GetTokenResponseJson.Invalid.ErrorModel(
|
||||
errorMessage = "123",
|
||||
|
||||
@ -19,7 +19,6 @@ class TwoFactorRequiredExtensionTest {
|
||||
),
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -42,7 +41,6 @@ class TwoFactorRequiredExtensionTest {
|
||||
),
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -58,7 +56,6 @@ class TwoFactorRequiredExtensionTest {
|
||||
),
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -71,7 +68,6 @@ class TwoFactorRequiredExtensionTest {
|
||||
authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -87,7 +83,6 @@ class TwoFactorRequiredExtensionTest {
|
||||
),
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -100,7 +95,6 @@ class TwoFactorRequiredExtensionTest {
|
||||
authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -116,7 +110,6 @@ class TwoFactorRequiredExtensionTest {
|
||||
),
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("Email" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -132,7 +125,6 @@ class TwoFactorRequiredExtensionTest {
|
||||
mapOf("AuthUrl" to JsonPrimitive(authUrl)),
|
||||
),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
@ -148,7 +140,6 @@ class TwoFactorRequiredExtensionTest {
|
||||
mapOf("AuthUrl" to JsonPrimitive(authUrl)),
|
||||
),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
|
||||
@ -483,7 +483,6 @@ Scanning will happen automatically.</string>
|
||||
<string name="password_prompt">Master password re-prompt</string>
|
||||
<string name="password_confirmation">Master password confirmation</string>
|
||||
<string name="password_confirmation_desc">This action is protected, to continue please re-enter your master password to verify your identity.</string>
|
||||
<string name="captcha_failed">Captcha failed. Please try again.</string>
|
||||
<string name="update_master_password">Update master password</string>
|
||||
<string name="update_master_password_warning">Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour.</string>
|
||||
<string name="updating_password">Updating password</string>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user