mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 11:18:45 +03:00
BIT-394 Setup service layer to accommodate get token error parsing (#61)
This commit is contained in:
parent
9d7990026c
commit
e69049d597
17 changed files with 477 additions and 50 deletions
|
@ -6,7 +6,7 @@ import retrofit2.http.Body
|
|||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines calls under the /accounts API.
|
||||
* Defines raw calls under the /accounts API.
|
||||
*/
|
||||
interface AccountsApi {
|
||||
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Defines calls under the /identity API.
|
||||
* Defines raw calls under the /identity API.
|
||||
*/
|
||||
interface IdentityApi {
|
||||
|
||||
|
@ -18,18 +16,15 @@ interface IdentityApi {
|
|||
@Suppress("LongParameterList")
|
||||
@FormUrlEncoded
|
||||
suspend fun getToken(
|
||||
// TODO: use correct base URL here BIT-328
|
||||
@Url url: String = "https://vault.bitwarden.com/identity/connect/token",
|
||||
@Field(value = "scope", encoded = true) scope: String = "api+offline_access",
|
||||
@Field(value = "client_id") clientId: String = "mobile",
|
||||
@Url url: String,
|
||||
@Field(value = "scope", encoded = true) scope: String,
|
||||
@Field(value = "client_id") clientId: String,
|
||||
@Field(value = "username") email: String,
|
||||
@Header(value = "auth-email") authEmail: String = email.base64UrlEncode(),
|
||||
@Header(value = "auth-email") authEmail: String,
|
||||
@Field(value = "password") passwordHash: String,
|
||||
// TODO: use correct device identifier here BIT-325
|
||||
@Field(value = "deviceIdentifier") deviceIdentifier: String = UUID.randomUUID().toString(),
|
||||
// TODO: use correct values for deviceName and deviceType BIT-326
|
||||
@Field(value = "deviceName") deviceName: String = "Pixel 6",
|
||||
@Field(value = "deviceType") deviceType: String = "1",
|
||||
@Field(value = "grant_type") grantType: String = "password",
|
||||
): Result<GetTokenResponseJson>
|
||||
@Field(value = "deviceIdentifier") deviceIdentifier: String,
|
||||
@Field(value = "deviceName") deviceName: String,
|
||||
@Field(value = "deviceType") deviceType: String,
|
||||
@Field(value = "grant_type") grantType: String,
|
||||
): Result<GetTokenResponseJson.Success>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.di
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsServiceImpl
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.NetworkModule
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides network dependencies in the auth package.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAccountService(
|
||||
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
|
||||
): AccountsService = AccountsServiceImpl(retrofit.create())
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIdentityService(
|
||||
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
|
||||
json: Json,
|
||||
): IdentityService = IdentityServiceImpl(retrofit.create(), json)
|
||||
}
|
|
@ -4,12 +4,26 @@ import kotlinx.serialization.SerialName
|
|||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models json response of the get token request.
|
||||
*
|
||||
* @param accessToken the access token.
|
||||
* Models response bodies from the get token request.
|
||||
*/
|
||||
@Serializable
|
||||
data class GetTokenResponseJson(
|
||||
@SerialName("access_token")
|
||||
val accessToken: String,
|
||||
)
|
||||
sealed class GetTokenResponseJson {
|
||||
/**
|
||||
* Models json response of the get token request.
|
||||
*
|
||||
* @param accessToken the access token.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
@SerialName("access_token")
|
||||
val accessToken: String,
|
||||
) : GetTokenResponseJson()
|
||||
|
||||
/**
|
||||
* Models json body of a captcha error.
|
||||
*/
|
||||
@Serializable
|
||||
data class CaptchaRequired(
|
||||
@SerialName("HCaptcha_SiteKey")
|
||||
val captchaKey: String,
|
||||
) : GetTokenResponseJson()
|
||||
}
|
||||
|
|
|
@ -12,6 +12,11 @@ sealed class LoginResult {
|
|||
*/
|
||||
data object Success : LoginResult()
|
||||
|
||||
/**
|
||||
* Captcha verification is required.
|
||||
*/
|
||||
data class CaptchaRequired(val captchaId: String) : LoginResult()
|
||||
|
||||
/**
|
||||
* There was an error logging in.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
|
||||
/**
|
||||
* Wraps raw retrofit accounts API in a cleaner interface.
|
||||
*/
|
||||
interface AccountsService {
|
||||
|
||||
/**
|
||||
* Make pre login request to get KDF params.
|
||||
*/
|
||||
suspend fun preLogin(email: String): Result<PreLoginResponseJson>
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
|
||||
class AccountsServiceImpl constructor(
|
||||
private val accountsApi: AccountsApi,
|
||||
) : AccountsService {
|
||||
|
||||
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
|
||||
accountsApi.preLogin(PreLoginRequestJson(email = email))
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
|
||||
/**
|
||||
* Wraps raw retrofit identity API in a cleaner interface.
|
||||
*/
|
||||
interface IdentityService {
|
||||
|
||||
/**
|
||||
* Make request to get an access token.
|
||||
*
|
||||
* @param email user's email address.
|
||||
* @param passwordHash password hashed with the Bitwarden SDK.
|
||||
*/
|
||||
suspend fun getToken(
|
||||
email: String,
|
||||
passwordHash: String,
|
||||
): Result<GetTokenResponseJson>
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyAsResult
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
|
||||
import java.util.UUID
|
||||
|
||||
class IdentityServiceImpl constructor(
|
||||
private val api: IdentityApi,
|
||||
private val json: Json,
|
||||
// TODO: use correct base URL here BIT-328
|
||||
private val baseUrl: String = "https://vault.bitwarden.com",
|
||||
) : IdentityService {
|
||||
|
||||
override suspend fun getToken(
|
||||
email: String,
|
||||
passwordHash: String,
|
||||
): Result<GetTokenResponseJson> = api
|
||||
.getToken(
|
||||
// TODO: use correct base URL here BIT-328
|
||||
url = "$baseUrl/identity/connect/token",
|
||||
scope = "api+offline_access",
|
||||
clientId = "mobile",
|
||||
authEmail = email.base64UrlEncode(),
|
||||
// TODO: use correct device identifier here BIT-325
|
||||
deviceIdentifier = UUID.randomUUID().toString(),
|
||||
// TODO: use correct values for deviceName and deviceType BIT-326
|
||||
deviceName = "Pixel 6",
|
||||
deviceType = "1",
|
||||
grantType = "password",
|
||||
passwordHash = passwordHash,
|
||||
email = email,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { Result.success(it) },
|
||||
onFailure = {
|
||||
it.parseErrorBodyAsResult<GetTokenResponseJson.CaptchaRequired>(
|
||||
code = HTTP_BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
|
@ -2,11 +2,12 @@ package com.x8bit.bitwarden.data.auth.repository
|
|||
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.CaptchaRequired
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.Success
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -20,8 +21,8 @@ import javax.inject.Singleton
|
|||
*/
|
||||
@Singleton
|
||||
class AuthRepositoryImpl @Inject constructor(
|
||||
private val accountsApi: AccountsApi,
|
||||
private val identityApi: IdentityApi,
|
||||
private val accountsService: AccountsService,
|
||||
private val identityService: IdentityService,
|
||||
private val bitwardenSdkClient: Client,
|
||||
private val authTokenInterceptor: AuthTokenInterceptor,
|
||||
) : AuthRepository {
|
||||
|
@ -35,8 +36,8 @@ class AuthRepositoryImpl @Inject constructor(
|
|||
override suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
): LoginResult = accountsApi
|
||||
.preLogin(PreLoginRequestJson(email))
|
||||
): LoginResult = accountsService
|
||||
.preLogin(email = email)
|
||||
.flatMap {
|
||||
// TODO: Use KDF enum from pre login correctly (BIT-329)
|
||||
val passwordHash = bitwardenSdkClient
|
||||
|
@ -46,21 +47,27 @@ class AuthRepositoryImpl @Inject constructor(
|
|||
password = password,
|
||||
kdfParams = Kdf.Pbkdf2(it.kdfIterations),
|
||||
)
|
||||
identityApi.getToken(
|
||||
identityService.getToken(
|
||||
email = email,
|
||||
passwordHash = passwordHash,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = {
|
||||
// TODO: Add more detail to these cases to expose server error messages (BIT-320)
|
||||
// TODO: Add more detail to error case to expose server error messages (BIT-320)
|
||||
LoginResult.Error
|
||||
},
|
||||
onSuccess = {
|
||||
// TODO: Create intermediate class for providing auth token to interceptor (BIT-411)
|
||||
authTokenInterceptor.authToken = it.accessToken
|
||||
mutableAuthStateFlow.value = AuthState.Authenticated(it.accessToken)
|
||||
LoginResult.Success
|
||||
when (it) {
|
||||
is CaptchaRequired -> LoginResult.CaptchaRequired(it.captchaKey)
|
||||
is Success -> {
|
||||
// TODO: Create intermediate class for providing auth token
|
||||
// to interceptor (BIT-411)
|
||||
authTokenInterceptor.authToken = it.accessToken
|
||||
mutableAuthStateFlow.value = AuthState.Authenticated(it.accessToken)
|
||||
LoginResult.Success
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.di
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.api.ConfigApi
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
|
@ -27,23 +25,23 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
private const val AUTHORIZED = "authorized"
|
||||
private const val UNAUTHORIZED = "unauthorized"
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAccountsApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): AccountsApi =
|
||||
retrofit.create()
|
||||
const val AUTHORIZED: String = "authorized"
|
||||
const val UNAUTHORIZED: String = "unauthorized"
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesConfigApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): ConfigApi =
|
||||
retrofit.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIdentityApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): IdentityApi =
|
||||
retrofit.create()
|
||||
fun provideOkHttpClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(
|
||||
HttpLoggingInterceptor().apply {
|
||||
setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import retrofit2.HttpException
|
||||
|
||||
/**
|
||||
* Attempt to parse the error body to serializable type [T].
|
||||
*
|
||||
* Useful in service layer for parsing non-200 response bodies.
|
||||
*
|
||||
* If the receiver is not an [HttpException] or the error body cannot be parsed, the original
|
||||
* Throwable will be returned as a Result.failure.
|
||||
*
|
||||
* @param code HTTP code associated with the error. Only responses with this code will be attempted
|
||||
* to be parsed.
|
||||
* @param json [Json] serializer to use.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
inline fun <reified T> Throwable.parseErrorBodyAsResult(code: Int, json: Json): Result<T> =
|
||||
(this as? HttpException)
|
||||
?.response()
|
||||
?.takeIf { it.code() == code }
|
||||
?.errorBody()
|
||||
?.let { errorBody ->
|
||||
try {
|
||||
Result.success(json.decodeFromStream(errorBody.byteStream()))
|
||||
} catch (_: Exception) {
|
||||
Result.failure(this)
|
||||
}
|
||||
} ?: Result.failure(this)
|
|
@ -60,6 +60,8 @@ class LoginViewModel @Inject constructor(
|
|||
LoginResult.Error -> Unit
|
||||
// No action required on success, root nav will navigate to logged in state
|
||||
LoginResult.Success -> Unit
|
||||
// TODO: launch intent with captcha URL BIT-399
|
||||
is LoginResult.CaptchaRequired -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.create
|
||||
|
||||
class AccountsServiceTest : BaseServiceTest() {
|
||||
|
||||
private val accountsApi: AccountsApi = retrofit.create()
|
||||
private val service = AccountsServiceImpl(accountsApi)
|
||||
|
||||
@Test
|
||||
fun `preLogin should call API`() = runTest {
|
||||
val response = MockResponse().setBody(PRE_LOGIN_RESPONSE_JSON)
|
||||
server.enqueue(response)
|
||||
assertEquals(Result.success(PRE_LOGIN_RESPONSE), service.preLogin(EMAIL))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "email"
|
||||
}
|
||||
}
|
||||
|
||||
private const val PRE_LOGIN_RESPONSE_JSON = """
|
||||
{
|
||||
"kdf": 1,
|
||||
"kdfIterations": 1,
|
||||
"kdfMemory": 1,
|
||||
"kdfParallelism": 1
|
||||
}
|
||||
"""
|
||||
|
||||
private val PRE_LOGIN_RESPONSE = PreLoginResponseJson(
|
||||
kdf = 1,
|
||||
kdfIterations = 1u,
|
||||
kdfMemory = 1,
|
||||
kdfParallelism = 1,
|
||||
)
|
|
@ -0,0 +1,63 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.create
|
||||
|
||||
class IdentityServiceTest : BaseServiceTest() {
|
||||
|
||||
private val identityApi: IdentityApi = retrofit.create()
|
||||
|
||||
private val identityService = IdentityServiceImpl(
|
||||
api = identityApi,
|
||||
json = Json,
|
||||
baseUrl = server.url("/").toString(),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `getToken when request response is Success should return Success`() = runTest {
|
||||
server.enqueue(MockResponse().setBody(LOGIN_SUCCESS_JSON))
|
||||
val result = identityService.getToken(email = EMAIL, passwordHash = PASSWORD_HASH)
|
||||
assertEquals(Result.success(LOGIN_SUCCESS), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getToken when request is error should return error`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(500))
|
||||
val result = identityService.getToken(email = EMAIL, passwordHash = PASSWORD_HASH)
|
||||
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, passwordHash = PASSWORD_HASH)
|
||||
assertEquals(Result.success(CAPTCHA_BODY), result)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "email"
|
||||
private const val PASSWORD_HASH = "passwordHash"
|
||||
}
|
||||
}
|
||||
|
||||
private const val CAPTCHA_BODY_JSON = """
|
||||
{
|
||||
"HCaptcha_SiteKey": "123"
|
||||
}
|
||||
"""
|
||||
private val CAPTCHA_BODY = GetTokenResponseJson.CaptchaRequired("123")
|
||||
|
||||
private const val LOGIN_SUCCESS_JSON = """
|
||||
{
|
||||
"access_token": "123"
|
||||
}
|
||||
"""
|
||||
private val LOGIN_SUCCESS = GetTokenResponseJson.Success("123")
|
|
@ -0,0 +1,110 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AuthRepositoryTest {
|
||||
|
||||
private val accountsService: AccountsService = mockk()
|
||||
private val identityService: IdentityService = mockk()
|
||||
private val authInterceptor = mockk<AuthTokenInterceptor>()
|
||||
private val mockBitwardenSdk = mockk<Client> {
|
||||
coEvery {
|
||||
auth().hashPassword(
|
||||
email = EMAIL,
|
||||
password = PASSWORD,
|
||||
kdfParams = Kdf.Pbkdf2(iterations = PRE_LOGIN_SUCCESS.kdfIterations),
|
||||
)
|
||||
} returns PASSWORD_HASH
|
||||
}
|
||||
|
||||
private val repository = AuthRepositoryImpl(
|
||||
accountsService = accountsService,
|
||||
identityService = identityService,
|
||||
bitwardenSdkClient = mockBitwardenSdk,
|
||||
authTokenInterceptor = authInterceptor,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
clearMocks(identityService, accountsService, authInterceptor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login when pre login fails should return Error`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns (Result.failure(RuntimeException()))
|
||||
val result = repository.login(EMAIL, PASSWORD)
|
||||
assertEquals(LoginResult.Error, result)
|
||||
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
|
||||
coVerify { accountsService.preLogin(EMAIL) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login get token fails should return Error`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery { identityService.getToken(EMAIL, PASSWORD_HASH) }
|
||||
.returns(Result.failure(RuntimeException()))
|
||||
val result = repository.login(EMAIL, PASSWORD)
|
||||
assertEquals(LoginResult.Error, result)
|
||||
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
|
||||
coVerify { accountsService.preLogin(EMAIL) }
|
||||
coVerify { identityService.getToken(EMAIL, PASSWORD_HASH) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login get token succeeds should return Success and update AuthState`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery { identityService.getToken(email = EMAIL, passwordHash = PASSWORD_HASH) }
|
||||
.returns(Result.success(GetTokenResponseJson.Success(accessToken = ACCESS_TOKEN)))
|
||||
every { authInterceptor.authToken = ACCESS_TOKEN } returns Unit
|
||||
val result = repository.login(EMAIL, PASSWORD)
|
||||
assertEquals(LoginResult.Success, result)
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||
verify { authInterceptor.authToken = ACCESS_TOKEN }
|
||||
coVerify { accountsService.preLogin(EMAIL) }
|
||||
coVerify { identityService.getToken(EMAIL, PASSWORD_HASH) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login get token returns captcha request should return CaptchaRequired`() = runTest {
|
||||
coEvery { accountsService.preLogin(EMAIL) } returns Result.success(PRE_LOGIN_SUCCESS)
|
||||
coEvery { identityService.getToken(email = EMAIL, passwordHash = PASSWORD_HASH) }
|
||||
.returns(Result.success(GetTokenResponseJson.CaptchaRequired(CAPTCHA_KEY)))
|
||||
val result = repository.login(EMAIL, PASSWORD)
|
||||
assertEquals(LoginResult.CaptchaRequired(CAPTCHA_KEY), result)
|
||||
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
|
||||
coVerify { accountsService.preLogin(EMAIL) }
|
||||
coVerify { identityService.getToken(EMAIL, PASSWORD_HASH) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@test.com"
|
||||
private const val PASSWORD = "password"
|
||||
private const val PASSWORD_HASH = "passwordHash"
|
||||
private const val ACCESS_TOKEN = "accessToken"
|
||||
private const val CAPTCHA_KEY = "captcha"
|
||||
private val PRE_LOGIN_SUCCESS = PreLoginResponseJson(
|
||||
kdf = 1,
|
||||
kdfIterations = 1u,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.x8bit.bitwarden.data.platform.base
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import retrofit2.Retrofit
|
||||
|
||||
/**
|
||||
* Base class for service tests. Provides common mock web server and retrofit setup.
|
||||
*/
|
||||
abstract class BaseServiceTest {
|
||||
|
||||
protected val server = MockWebServer().apply { start() }
|
||||
|
||||
protected val retrofit: Retrofit = Retrofit.Builder()
|
||||
.baseUrl(server.url("/").toString())
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
server.shutdown()
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue