[PM-6702] 1# Add service calls for email verification (#3617)

This commit is contained in:
André Bispo 2024-08-09 19:38:52 +01:00 committed by GitHub
parent edb87202d2
commit e717183239
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 526 additions and 52 deletions

View file

@ -5,8 +5,11 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJso
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import kotlinx.serialization.json.JsonPrimitive
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Field
@ -66,4 +69,14 @@ interface IdentityApi {
@POST("/accounts/register")
suspend fun register(@Body body: RegisterRequestJson): Result<RegisterResponseJson.Success>
@POST("/accounts/register/finish")
suspend fun registerFinish(
@Body body: RegisterFinishRequestJson,
): Result<RegisterResponseJson.Success>
@POST("/accounts/register/send-verification-email")
suspend fun sendVerificationEmail(
@Body body: SendVerificationEmailRequestJson,
): Result<JsonPrimitive?>
}

View file

@ -0,0 +1,64 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson.Keys
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Request body for register.
*
* @param email the email to be registered.
* @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].
* @param kdfIterations the number of kdf iterations.
*/
@Serializable
data class RegisterFinishRequestJson(
@SerialName("email")
val email: String,
@SerialName("emailVerificationToken")
val emailVerificationToken: String,
@SerialName("masterPasswordHash")
val masterPasswordHash: String,
@SerialName("masterPasswordHint")
val masterPasswordHint: String?,
@SerialName("captchaResponse")
val captchaResponse: String?,
@SerialName("userSymmetricKey")
val userSymmetricKey: String,
@SerialName("userAsymmetricKeys")
val userAsymmetricKeys: Keys,
@SerialName("kdf")
val kdfType: KdfTypeJson,
@SerialName("kdfIterations")
val kdfIterations: UInt,
) {
/**
* A keys object containing public and private keys.
*
* @param publicKey the public key (encrypted).
* @param encryptedPrivateKey the private key (encrypted).
*/
@Serializable
data class Keys(
@SerialName("publicKey")
val publicKey: String,
@SerialName("encryptedPrivateKey")
val encryptedPrivateKey: String,
)
}

View file

@ -0,0 +1,23 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Request body for send verification email.
*
* @param email the email to be registered.
* @param name the name to be registered.
* @param receiveMarketingEmails the answer to receive marketing emails.
*/
@Serializable
data class SendVerificationEmailRequestJson(
@SerialName("email")
val email: String,
@SerialName("name")
val name: String?,
@SerialName("receiveMarketingEmails")
val receiveMarketingEmails: Boolean,
)

View file

@ -0,0 +1,47 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* The response body for sending a verification email.
*/
@Serializable
sealed class SendVerificationEmailResponseJson {
/**
* Models a successful json response.
*
* @param emailVerificationToken the token to verify the email.
*/
@Serializable
data class Success(
val emailVerificationToken: String?,
) : SendVerificationEmailResponseJson()
/**
* Represents the json body of an invalid request.
*
* @param 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")
val message: String?,
@SerialName("validationErrors")
val validationErrors: Map<String, List<String>>?,
) : SendVerificationEmailResponseJson()
/**
* A different error with a message.
*/
@Serializable
data class Error(
@SerialName("Message")
val message: String?,
) : SendVerificationEmailResponseJson()
}

View file

@ -5,8 +5,10 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthM
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
/**
@ -58,4 +60,16 @@ interface IdentityService {
* @param refreshToken The refresh token needed to obtain a new token.
*/
fun refreshTokenSynchronously(refreshToken: String): Result<RefreshTokenResponseJson>
/**
* Send a verification email.
*/
suspend fun sendVerificationEmail(
body: SendVerificationEmailRequestJson,
): Result<String?>
/**
* Register a new account to Bitwarden using email verification flow.
*/
suspend fun registerFinish(body: RegisterFinishRequestJson): Result<RegisterResponseJson>
}

View file

@ -7,8 +7,10 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJso
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
@ -32,16 +34,21 @@ class IdentityServiceImpl(
.register(body)
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
code = 400,
json = json,
) ?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
codes = listOf(400, 429),
json = json,
) ?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
code = 429,
json = json,
) ?: throw throwable
bitwardenError
.parseErrorBodyOrNull<RegisterResponseJson.CaptchaRequired>(
code = 400,
json = json,
)
?: bitwardenError
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
codes = listOf(400, 429),
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
code = 429,
json = json,
)
?: throw throwable
}
@Suppress("MagicNumber")
@ -101,4 +108,32 @@ class IdentityServiceImpl(
refreshToken = refreshToken,
)
.executeForResult()
@Suppress("MagicNumber")
override suspend fun registerFinish(
body: RegisterFinishRequestJson,
): Result<RegisterResponseJson> =
api
.registerFinish(body)
.recoverCatching { throwable ->
val bitwardenError = throwable.toBitwardenError()
bitwardenError
.parseErrorBodyOrNull<RegisterResponseJson.Invalid>(
codes = listOf(400, 429),
json = json,
)
?: bitwardenError.parseErrorBodyOrNull<RegisterResponseJson.Error>(
code = 429,
json = json,
)
?: throw throwable
}
override suspend fun sendVerificationEmail(
body: SendVerificationEmailRequestJson,
): Result<String?> {
return api
.sendVerificationEmail(body = body)
.map { it?.content }
}
}

View file

@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
@ -253,6 +254,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
email: String,
masterPassword: String,
masterPasswordHint: String?,
emailVerificationToken: String? = null,
captchaToken: String?,
shouldCheckDataBreaches: Boolean,
isMasterPasswordStrong: Boolean,
@ -354,4 +356,13 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
* policies for the current user.
*/
suspend fun validatePasswordAgainstPolicies(password: String): Boolean
/**
* Send a verification email.
*/
suspend fun sendVerificationEmail(
email: String,
name: String,
receiveMarketingEmails: Boolean,
): SendVerificationEmailResult
}

View file

@ -16,10 +16,12 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJs
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
@ -50,6 +52,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
@ -723,6 +726,7 @@ class AuthRepositoryImpl(
email: String,
masterPassword: String,
masterPasswordHint: String?,
emailVerificationToken: String?,
captchaToken: String?,
shouldCheckDataBreaches: Boolean,
isMasterPasswordStrong: Boolean,
@ -751,21 +755,40 @@ class AuthRepositoryImpl(
kdf = kdf,
)
.flatMap { registerKeyResponse ->
identityService.register(
body = RegisterRequestJson(
email = email,
masterPasswordHash = registerKeyResponse.masterPasswordHash,
masterPasswordHint = masterPasswordHint,
captchaResponse = captchaToken,
key = registerKeyResponse.encryptedUserKey,
keys = RegisterRequestJson.Keys(
publicKey = registerKeyResponse.keys.public,
encryptedPrivateKey = registerKeyResponse.keys.private,
if (emailVerificationToken == null) {
identityService.register(
body = RegisterRequestJson(
email = email,
masterPasswordHash = registerKeyResponse.masterPasswordHash,
masterPasswordHint = masterPasswordHint,
captchaResponse = captchaToken,
key = registerKeyResponse.encryptedUserKey,
keys = RegisterRequestJson.Keys(
publicKey = registerKeyResponse.keys.public,
encryptedPrivateKey = registerKeyResponse.keys.private,
),
kdfType = kdf.toKdfTypeJson(),
kdfIterations = kdf.iterations,
),
kdfType = kdf.toKdfTypeJson(),
kdfIterations = kdf.iterations,
),
)
)
} else {
identityService.registerFinish(
body = RegisterFinishRequestJson(
email = email,
masterPasswordHash = registerKeyResponse.masterPasswordHash,
masterPasswordHint = masterPasswordHint,
emailVerificationToken = emailVerificationToken,
captchaResponse = captchaToken,
userSymmetricKey = registerKeyResponse.encryptedUserKey,
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
publicKey = registerKeyResponse.keys.public,
encryptedPrivateKey = registerKeyResponse.keys.private,
),
kdfType = kdf.toKdfTypeJson(),
kdfIterations = kdf.iterations,
),
)
}
}
.fold(
onSuccess = {
@ -1159,6 +1182,28 @@ class AuthRepositoryImpl(
): Boolean = passwordPolicies
.all { validatePasswordAgainstPolicy(password, it) }
override suspend fun sendVerificationEmail(
email: String,
name: String,
receiveMarketingEmails: Boolean,
): SendVerificationEmailResult =
identityService
.sendVerificationEmail(
SendVerificationEmailRequestJson(
email = email,
name = name,
receiveMarketingEmails = receiveMarketingEmails,
),
)
.fold(
onSuccess = {
SendVerificationEmailResult.Success(it)
},
onFailure = {
SendVerificationEmailResult.Error(null)
},
)
@Suppress("CyclomaticComplexMethod")
private suspend fun validatePasswordAgainstPolicy(
password: String,

View file

@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Models result of sending a verification email.
*/
sealed class SendVerificationEmailResult {
/**
* Email sent succeeded.
*
* @param emailVerificationToken the token to verify the email.
*/
data class Success(
val emailVerificationToken: String?,
) : SendVerificationEmailResult()
/**
* There was an error sending the email.
*
* @param errorMessage a message describing the error.
*/
data class Error(val errorMessage: String?) : SendVerificationEmailResult()
}

View file

@ -9,8 +9,10 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.MasterPasswordPoli
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
@ -132,15 +134,10 @@ class IdentityServiceTest : BaseServiceTest() {
@Test
fun `register success json should be Success`() = runTest {
val json = """
{
"captchaBypassToken": "mock_token"
}
"""
val expectedResponse = RegisterResponseJson.Success(
captchaBypassToken = "mock_token",
)
val response = MockResponse().setBody(json)
val response = MockResponse().setBody(CAPTCHA_BYPASS_TOKEN_RESPONSE_JSON)
server.enqueue(response)
assertEquals(
expectedResponse.asSuccess(),
@ -150,21 +147,9 @@ class IdentityServiceTest : BaseServiceTest() {
@Test
fun `register failure with Invalid json should be Invalid`() = runTest {
val json = """
{
"message": "The model state is invalid.",
"validationErrors": {
"": [
"Email '' is already taken."
]
},
"exceptionMessage": null,
"exceptionStackTrace": null,
"innerExceptionMessage": null,
"object": "error"
}
"""
val response = MockResponse().setResponseCode(400).setBody(json)
val response = MockResponse().setResponseCode(400).setBody(
INVALID_MODEL_STATE_EMAIL_TAKEN_ERROR_JSON,
)
server.enqueue(response)
val result = identityService.register(registerRequestBody)
assertEquals(
@ -178,13 +163,7 @@ class IdentityServiceTest : BaseServiceTest() {
@Test
fun `register failure with Error json should return Error`() = runTest {
val json = """
{
"Object": "error",
"Message": "Slow down! Too many requests. Try again soon."
}
""".trimIndent()
val response = MockResponse().setResponseCode(429).setBody(json)
val response = MockResponse().setResponseCode(429).setBody(TOO_MANY_REQUEST_ERROR_JSON)
server.enqueue(response)
val result = identityService.register(registerRequestBody)
assertEquals(
@ -327,9 +306,74 @@ class IdentityServiceTest : BaseServiceTest() {
assertTrue(result.isFailure)
}
@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)
server.enqueue(response)
assertEquals(
expectedResponse.asSuccess(),
identityService.registerFinish(registerFinishRequestBody),
)
}
@Test
fun `registerFinish failure with Invalid json should be Invalid`() = runTest {
val response = MockResponse().setResponseCode(400).setBody(
INVALID_MODEL_STATE_EMAIL_TAKEN_ERROR_JSON,
)
server.enqueue(response)
val result = identityService.registerFinish(registerFinishRequestBody)
assertEquals(
RegisterResponseJson.Invalid(
message = "The model state is invalid.",
validationErrors = mapOf("" to listOf("Email '' is already taken.")),
),
result.getOrThrow(),
)
}
@Test
fun `registerFinish failure with Error json should return Error`() = runTest {
val response = MockResponse().setResponseCode(429).setBody(TOO_MANY_REQUEST_ERROR_JSON)
server.enqueue(response)
val result = identityService.registerFinish(registerFinishRequestBody)
assertEquals(
RegisterResponseJson.Error(
message = "Slow down! Too many requests. Try again soon.",
),
result.getOrThrow(),
)
}
@Test
fun `sendVerificationEmail should return a string when response is populated success`() =
runTest {
server.enqueue(MockResponse().setResponseCode(200).setBody(EMAIL_TOKEN))
val result = identityService.sendVerificationEmail(SEND_VERIFICATION_EMAIL_REQUEST)
assertEquals(JsonPrimitive(EMAIL_TOKEN).content.asSuccess(), result)
}
@Test
fun `sendVerificationEmail should return null when response is empty success`() = runTest {
server.enqueue(MockResponse().setResponseCode(204))
val result = identityService.sendVerificationEmail(SEND_VERIFICATION_EMAIL_REQUEST)
assertEquals(null.asSuccess(), result)
}
@Test
fun `sendVerificationEmail should return an error when response is an error`() = runTest {
server.enqueue(MockResponse().setResponseCode(400))
val result = identityService.sendVerificationEmail(SEND_VERIFICATION_EMAIL_REQUEST)
assertTrue(result.isFailure)
}
companion object {
private const val UNIQUE_APP_ID = "testUniqueAppId"
private const val REFRESH_TOKEN = "refreshToken"
private const val EMAIL_TOKEN = "emailToken"
private const val EMAIL = "email"
private const val PASSWORD_HASH = "passwordHash"
private val registerRequestBody = RegisterRequestJson(
@ -345,6 +389,20 @@ class IdentityServiceTest : BaseServiceTest() {
kdfType = KdfTypeJson.PBKDF2_SHA256,
kdfIterations = 600000U,
)
private val registerFinishRequestBody = RegisterFinishRequestJson(
email = EMAIL,
masterPasswordHash = "mockk_masterPasswordHash",
masterPasswordHint = "mockk_masterPasswordHint",
emailVerificationToken = "mock_emailVerificationToken",
captchaResponse = "mockk_captchaResponse",
userSymmetricKey = "mockk_key",
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
publicKey = "mockk_publicKey",
encryptedPrivateKey = "mockk_encryptedPrivateKey",
),
kdfType = KdfTypeJson.PBKDF2_SHA256,
kdfIterations = 600000U,
)
}
}
@ -484,8 +542,42 @@ private const val INVALID_LOGIN_JSON = """
}
"""
private const val TOO_MANY_REQUEST_ERROR_JSON = """
{
"Object": "error",
"Message": "Slow down! Too many requests. Try again soon."
}
"""
private const val INVALID_MODEL_STATE_EMAIL_TAKEN_ERROR_JSON = """
{
"message": "The model state is invalid.",
"validationErrors": {
"": [
"Email '' is already taken."
]
},
"exceptionMessage": null,
"exceptionStackTrace": null,
"innerExceptionMessage": null,
"object": "error"
}
"""
private const val CAPTCHA_BYPASS_TOKEN_RESPONSE_JSON = """
{
"captchaBypassToken": "mock_token"
}
"""
private val INVALID_LOGIN = GetTokenResponseJson.Invalid(
errorModel = GetTokenResponseJson.Invalid.ErrorModel(
errorMessage = "123",
),
)
private val SEND_VERIFICATION_EMAIL_REQUEST = SendVerificationEmailRequestJson(
email = "email@example.com",
name = "Name Example",
receiveMarketingEmails = true,
)

View file

@ -29,10 +29,12 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRespon
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
@ -67,6 +69,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
@ -3791,6 +3794,40 @@ class AuthRepositoryTest {
assertEquals(RegisterResult.Error(errorMessage = "message"), result)
}
@Test
fun `register with email token Success should return Success`() = runTest {
coEvery { identityService.preLogin(EMAIL) } returns PRE_LOGIN_SUCCESS.asSuccess()
coEvery {
identityService.registerFinish(
body = RegisterFinishRequestJson(
email = EMAIL,
masterPasswordHash = PASSWORD_HASH,
masterPasswordHint = null,
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
captchaResponse = null,
userSymmetricKey = ENCRYPTED_USER_KEY,
userAsymmetricKeys = RegisterFinishRequestJson.Keys(
publicKey = PUBLIC_KEY,
encryptedPrivateKey = PRIVATE_KEY,
),
kdfType = KdfTypeJson.PBKDF2_SHA256,
kdfIterations = DEFAULT_KDF_ITERATIONS.toUInt(),
),
)
} returns RegisterResponseJson.Success(captchaBypassToken = CAPTCHA_KEY).asSuccess()
val result = repository.register(
email = EMAIL,
masterPassword = PASSWORD,
masterPasswordHint = null,
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
captchaToken = null,
shouldCheckDataBreaches = false,
isMasterPasswordStrong = true,
)
assertEquals(RegisterResult.Success(CAPTCHA_KEY), result)
}
@Test
fun `resetPassword Success should return Success`() = runTest {
val currentPassword = "currentPassword"
@ -5337,10 +5374,81 @@ class AuthRepositoryTest {
assertFalse(repository.validatePasswordAgainstPolicies(password = "letters"))
}
@Test
fun `sendVerificationEmail success should return success`() = runTest {
coEvery {
identityService.sendVerificationEmail(
SendVerificationEmailRequestJson(
email = EMAIL,
name = NAME,
receiveMarketingEmails = true,
),
)
} returns EMAIL_VERIFICATION_TOKEN.asSuccess()
val result = repository.sendVerificationEmail(
email = EMAIL,
name = NAME,
receiveMarketingEmails = true,
)
assertEquals(
SendVerificationEmailResult.Success(EMAIL_VERIFICATION_TOKEN),
result,
)
}
@Test
fun `sendVerificationEmail failure should return success if body null`() = runTest {
coEvery {
identityService.sendVerificationEmail(
SendVerificationEmailRequestJson(
email = EMAIL,
name = NAME,
receiveMarketingEmails = true,
),
)
} returns null.asSuccess()
val result = repository.sendVerificationEmail(
email = EMAIL,
name = NAME,
receiveMarketingEmails = true,
)
assertEquals(
SendVerificationEmailResult.Success(null),
result,
)
}
@Test
fun `sendVerificationEmail failure should return error`() = runTest {
coEvery {
identityService.sendVerificationEmail(
SendVerificationEmailRequestJson(
email = EMAIL,
name = NAME,
receiveMarketingEmails = true,
),
)
} returns Throwable("fail").asFailure()
val result = repository.sendVerificationEmail(
email = EMAIL,
name = NAME,
receiveMarketingEmails = true,
)
assertEquals(
SendVerificationEmailResult.Error(null),
result,
)
}
companion object {
private const val UNIQUE_APP_ID = "testUniqueAppId"
private const val NAME = "Example Name"
private const val EMAIL = "test@bitwarden.com"
private const val EMAIL_2 = "test2@bitwarden.com"
private const val EMAIL_VERIFICATION_TOKEN = "thisisanawesometoken"
private const val PASSWORD = "password"
private const val PASSWORD_HASH = "passwordHash"
private const val ACCESS_TOKEN = "accessToken"