mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 20:07:59 -06:00
PM-25908: Process 400 responses from verification code APIs (#5900)
This commit is contained in:
parent
b4a31764c4
commit
4f244c52fa
@ -33,6 +33,8 @@ import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.TwoFactorAuthMethod
|
||||
import com.bitwarden.network.model.TwoFactorDataModel
|
||||
import com.bitwarden.network.model.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerificationOtpResponseJson
|
||||
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
|
||||
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
@ -740,7 +742,17 @@ class AuthRepositoryImpl(
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationCodeResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationCodeResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
@ -753,7 +765,17 @@ class AuthRepositoryImpl(
|
||||
?.let { jsonRequest ->
|
||||
accountsService.resendNewDeviceOtp(body = jsonRequest).fold(
|
||||
onFailure = { ResendEmailResult.Error(message = it.message, error = it) },
|
||||
onSuccess = { ResendEmailResult.Success },
|
||||
onSuccess = {
|
||||
when (it) {
|
||||
VerificationOtpResponseJson.Success -> ResendEmailResult.Success
|
||||
is VerificationOtpResponseJson.Invalid -> {
|
||||
ResendEmailResult.Error(
|
||||
message = it.firstValidationErrorMessage,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
?: ResendEmailResult.Error(
|
||||
|
||||
@ -15,6 +15,6 @@ sealed class ResendEmailResult {
|
||||
*/
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable,
|
||||
val error: Throwable?,
|
||||
) : ResendEmailResult()
|
||||
}
|
||||
|
||||
@ -407,7 +407,8 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
it.copy(
|
||||
dialogState = TwoFactorLoginState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString.verification_email_not_sent.asText(),
|
||||
message = result.message?.asText()
|
||||
?: BitwardenString.verification_email_not_sent.asText(),
|
||||
error = result.error,
|
||||
),
|
||||
)
|
||||
|
||||
@ -48,6 +48,7 @@ import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.TwoFactorAuthMethod
|
||||
import com.bitwarden.network.model.TwoFactorDataModel
|
||||
import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerifiedOrganizationDomainSsoDetailsResponse
|
||||
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
|
||||
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
|
||||
@ -5887,7 +5888,7 @@ class AuthRepositoryTest {
|
||||
ssoToken = null,
|
||||
),
|
||||
)
|
||||
} returns Unit.asSuccess()
|
||||
} returns VerificationCodeResponseJson.Success.asSuccess()
|
||||
val resendEmailResult = repository.resendVerificationCodeEmail()
|
||||
assertEquals(ResendEmailResult.Success, resendEmailResult)
|
||||
coVerify {
|
||||
@ -5902,6 +5903,73 @@ class AuthRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resendVerificationCodeEmail uses cached request data to make api call with error`() =
|
||||
runTest {
|
||||
// Attempt a normal login with a two factor error first, so that the necessary
|
||||
// data will be cached.
|
||||
coEvery { identityService.preLogin(EMAIL) } returns PRE_LOGIN_SUCCESS.asSuccess()
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.MasterPassword(
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
} returns GetTokenResponseJson
|
||||
.TwoFactorRequired(
|
||||
authMethodsData = TWO_FACTOR_AUTH_METHODS_DATA,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
.asSuccess()
|
||||
val firstResult = repository.login(email = EMAIL, password = PASSWORD)
|
||||
assertEquals(LoginResult.TwoFactorRequired, firstResult)
|
||||
coVerify { identityService.preLogin(email = EMAIL) }
|
||||
coVerify {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.MasterPassword(
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
}
|
||||
|
||||
// Resend the verification code email.
|
||||
val message = "Failure Message"
|
||||
coEvery {
|
||||
accountsService.resendVerificationCodeEmail(
|
||||
body = ResendEmailRequestJson(
|
||||
deviceIdentifier = UNIQUE_APP_ID,
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
ssoToken = null,
|
||||
),
|
||||
)
|
||||
} returns VerificationCodeResponseJson
|
||||
.Invalid(message = message, validationErrors = null)
|
||||
.asSuccess()
|
||||
val resendEmailResult = repository.resendVerificationCodeEmail()
|
||||
assertEquals(
|
||||
ResendEmailResult.Error(message = message, error = null),
|
||||
resendEmailResult,
|
||||
)
|
||||
coVerify {
|
||||
accountsService.resendVerificationCodeEmail(
|
||||
body = ResendEmailRequestJson(
|
||||
deviceIdentifier = UNIQUE_APP_ID,
|
||||
email = EMAIL,
|
||||
passwordHash = PASSWORD_HASH,
|
||||
ssoToken = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resendVerificationCodeEmail returns error if no request data cached`() = runTest {
|
||||
val result = repository.resendVerificationCodeEmail()
|
||||
|
||||
@ -21,8 +21,9 @@ sealed interface InvalidJsonResponse {
|
||||
* Returns the first error message found in [validationErrors], or [message] if there are no
|
||||
* [validationErrors] present.
|
||||
*/
|
||||
val firstValidationErrorMessage: String?
|
||||
val firstValidationErrorMessage: String
|
||||
get() = validationErrors
|
||||
?.flatMap { it.value }
|
||||
?.first()
|
||||
?.firstOrNull()
|
||||
?: message
|
||||
}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models response bodies from verification code response.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class VerificationCodeResponseJson {
|
||||
/**
|
||||
* The success body of the verification code response.
|
||||
*/
|
||||
@Serializable
|
||||
data object Success : VerificationCodeResponseJson()
|
||||
|
||||
/**
|
||||
* Models the json body of verification code error.
|
||||
*
|
||||
* @param message a human readable error message.
|
||||
* @param validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
override val message: String,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
override val validationErrors: Map<String, List<String>>?,
|
||||
) : VerificationCodeResponseJson(), InvalidJsonResponse
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models response bodies from verification OTP response.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class VerificationOtpResponseJson {
|
||||
/**
|
||||
* The success body of the verification OTP response.
|
||||
*/
|
||||
@Serializable
|
||||
data object Success : VerificationOtpResponseJson()
|
||||
|
||||
/**
|
||||
* Models the json body of verification OTP error.
|
||||
*
|
||||
* @param message a human readable error message.
|
||||
* @param validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
override val message: String,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
override val validationErrors: Map<String, List<String>>?,
|
||||
) : VerificationOtpResponseJson(), InvalidJsonResponse
|
||||
}
|
||||
@ -8,6 +8,8 @@ import com.bitwarden.network.model.ResendEmailRequestJson
|
||||
import com.bitwarden.network.model.ResendNewDeviceOtpRequestJson
|
||||
import com.bitwarden.network.model.ResetPasswordRequestJson
|
||||
import com.bitwarden.network.model.SetPasswordRequestJson
|
||||
import com.bitwarden.network.model.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerificationOtpResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for querying accounts endpoints.
|
||||
@ -51,12 +53,16 @@ interface AccountsService {
|
||||
/**
|
||||
* Resend the email with the two-factor verification code.
|
||||
*/
|
||||
suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit>
|
||||
suspend fun resendVerificationCodeEmail(
|
||||
body: ResendEmailRequestJson,
|
||||
): Result<VerificationCodeResponseJson>
|
||||
|
||||
/**
|
||||
* Resend the email with the verification code for new devices
|
||||
*/
|
||||
suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit>
|
||||
suspend fun resendNewDeviceOtp(
|
||||
body: ResendNewDeviceOtpRequestJson,
|
||||
): Result<VerificationOtpResponseJson>
|
||||
|
||||
/**
|
||||
* Reset the password.
|
||||
|
||||
@ -16,6 +16,8 @@ import com.bitwarden.network.model.ResendEmailRequestJson
|
||||
import com.bitwarden.network.model.ResendNewDeviceOtpRequestJson
|
||||
import com.bitwarden.network.model.ResetPasswordRequestJson
|
||||
import com.bitwarden.network.model.SetPasswordRequestJson
|
||||
import com.bitwarden.network.model.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerificationOtpResponseJson
|
||||
import com.bitwarden.network.model.VerifyOtpRequestJson
|
||||
import com.bitwarden.network.model.toBitwardenError
|
||||
import com.bitwarden.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
@ -111,15 +113,39 @@ internal class AccountsServiceImpl(
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
|
||||
override suspend fun resendVerificationCodeEmail(
|
||||
body: ResendEmailRequestJson,
|
||||
): Result<VerificationCodeResponseJson> =
|
||||
unauthenticatedAccountsApi
|
||||
.resendVerificationCodeEmail(body = body)
|
||||
.toResult()
|
||||
.map { VerificationCodeResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<VerificationCodeResponseJson.Invalid>(
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override suspend fun resendNewDeviceOtp(body: ResendNewDeviceOtpRequestJson): Result<Unit> =
|
||||
override suspend fun resendNewDeviceOtp(
|
||||
body: ResendNewDeviceOtpRequestJson,
|
||||
): Result<VerificationOtpResponseJson> =
|
||||
unauthenticatedAccountsApi
|
||||
.resendNewDeviceOtp(body = body)
|
||||
.toResult()
|
||||
.map { VerificationOtpResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
throwable
|
||||
.toBitwardenError()
|
||||
.parseErrorBodyOrNull<VerificationOtpResponseJson.Invalid>(
|
||||
code = NetworkErrorCode.BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
|
||||
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
|
||||
if (body.currentPasswordHash == null) {
|
||||
|
||||
@ -15,6 +15,8 @@ import com.bitwarden.network.model.ResendEmailRequestJson
|
||||
import com.bitwarden.network.model.ResendNewDeviceOtpRequestJson
|
||||
import com.bitwarden.network.model.ResetPasswordRequestJson
|
||||
import com.bitwarden.network.model.SetPasswordRequestJson
|
||||
import com.bitwarden.network.model.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerificationOtpResponseJson
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@ -136,7 +138,30 @@ class AccountsServiceTest : BaseServiceTest() {
|
||||
ssoToken = null,
|
||||
),
|
||||
)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(VerificationCodeResponseJson.Success.asSuccess(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resendVerificationCodeEmail with 400 response is Error`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400).setBody(INVALID_JSON)
|
||||
server.enqueue(response)
|
||||
val result = service.resendVerificationCodeEmail(
|
||||
body = ResendEmailRequestJson(
|
||||
deviceIdentifier = "3",
|
||||
email = "example@email.com",
|
||||
passwordHash = "37y4d8r379r4789nt387r39k3dr87nr93",
|
||||
ssoToken = null,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
VerificationCodeResponseJson
|
||||
.Invalid(
|
||||
message = "User verification failed.",
|
||||
validationErrors = null,
|
||||
)
|
||||
.asSuccess(),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -264,4 +289,32 @@ class AccountsServiceTest : BaseServiceTest() {
|
||||
)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resendNewDeviceOtp with 400 response is Error`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400).setBody(INVALID_JSON)
|
||||
server.enqueue(response)
|
||||
val result = service.resendNewDeviceOtp(
|
||||
body = ResendNewDeviceOtpRequestJson(
|
||||
email = "example@email.com",
|
||||
passwordHash = "37y4d8r379r4789nt387r39k3dr87nr93",
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
VerificationOtpResponseJson
|
||||
.Invalid(
|
||||
message = "User verification failed.",
|
||||
validationErrors = null,
|
||||
)
|
||||
.asSuccess(),
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val INVALID_JSON = """
|
||||
{
|
||||
"message": "User verification failed.",
|
||||
"validationErrors": null
|
||||
}
|
||||
"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user