From e7171832398fcb59c4f34695e8a03dd62a10b7ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Fri, 9 Aug 2024 19:38:52 +0100 Subject: [PATCH] [PM-6702] 1# Add service calls for email verification (#3617) --- .../datasource/network/api/IdentityApi.kt | 13 ++ .../model/RegisterFinishRequestJson.kt | 64 ++++++++ .../model/SendVerificationEmailRequestJson.kt | 23 +++ .../SendVerificationEmailResponseJson.kt | 47 ++++++ .../network/service/IdentityService.kt | 14 ++ .../network/service/IdentityServiceImpl.kt | 55 +++++-- .../data/auth/repository/AuthRepository.kt | 11 ++ .../auth/repository/AuthRepositoryImpl.kt | 73 +++++++-- .../model/SendVerificationEmailResult.kt | 22 +++ .../network/service/IdentityServiceTest.kt | 148 ++++++++++++++---- .../auth/repository/AuthRepositoryTest.kt | 108 +++++++++++++ 11 files changed, 526 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/RegisterFinishRequestJson.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/SendVerificationEmailRequestJson.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/SendVerificationEmailResponseJson.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/SendVerificationEmailResult.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt index cc573398b..22ac1b4da 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt @@ -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 + + @POST("/accounts/register/finish") + suspend fun registerFinish( + @Body body: RegisterFinishRequestJson, + ): Result + + @POST("/accounts/register/send-verification-email") + suspend fun sendVerificationEmail( + @Body body: SendVerificationEmailRequestJson, + ): Result } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/RegisterFinishRequestJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/RegisterFinishRequestJson.kt new file mode 100644 index 000000000..bed89d1f5 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/RegisterFinishRequestJson.kt @@ -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, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/SendVerificationEmailRequestJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/SendVerificationEmailRequestJson.kt new file mode 100644 index 000000000..3de82b2ed --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/SendVerificationEmailRequestJson.kt @@ -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, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/SendVerificationEmailResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/SendVerificationEmailResponseJson.kt new file mode 100644 index 000000000..12928152c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/SendVerificationEmailResponseJson.kt @@ -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>?, + ) : SendVerificationEmailResponseJson() + + /** + * A different error with a message. + */ + @Serializable + data class Error( + @SerialName("Message") + val message: String?, + ) : SendVerificationEmailResponseJson() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt index 557193d98..1c2ada0dc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt @@ -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 + + /** + * Send a verification email. + */ + suspend fun sendVerificationEmail( + body: SendVerificationEmailRequestJson, + ): Result + + /** + * Register a new account to Bitwarden using email verification flow. + */ + suspend fun registerFinish(body: RegisterFinishRequestJson): Result } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt index 82d13e6ad..ec67abb2d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt @@ -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( - code = 400, - json = json, - ) ?: bitwardenError.parseErrorBodyOrNull( - codes = listOf(400, 429), - json = json, - ) ?: bitwardenError.parseErrorBodyOrNull( - code = 429, - json = json, - ) ?: throw throwable + bitwardenError + .parseErrorBodyOrNull( + code = 400, + json = json, + ) + ?: bitwardenError + .parseErrorBodyOrNull( + codes = listOf(400, 429), + json = json, + ) + ?: bitwardenError.parseErrorBodyOrNull( + 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 = + api + .registerFinish(body) + .recoverCatching { throwable -> + val bitwardenError = throwable.toBitwardenError() + bitwardenError + .parseErrorBodyOrNull( + codes = listOf(400, 429), + json = json, + ) + ?: bitwardenError.parseErrorBodyOrNull( + code = 429, + json = json, + ) + ?: throw throwable + } + + override suspend fun sendVerificationEmail( + body: SendVerificationEmailRequestJson, + ): Result { + return api + .sendVerificationEmail(body = body) + .map { it?.content } + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt index ac20b13f7..db39b8cdf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -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 } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index a020da031..9f2a35176 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/SendVerificationEmailResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/SendVerificationEmailResult.kt new file mode 100644 index 000000000..1c1ff789d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/SendVerificationEmailResult.kt @@ -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() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt index b524c2340..f020466c5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt @@ -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, +) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 92cd83469..e1e3aff16 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -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"