mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BIT-133 Implement happy path login (#52)
This commit is contained in:
parent
36942ab296
commit
14d01877fe
16 changed files with 432 additions and 33 deletions
|
@ -0,0 +1,18 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines calls under the /accounts API.
|
||||
*/
|
||||
interface AccountsApi {
|
||||
|
||||
/**
|
||||
* Make pre login request to get KDF params.
|
||||
*/
|
||||
@POST("/accounts/prelogin")
|
||||
suspend fun preLogin(@Body body: PreLoginRequestJson): Result<PreLoginResponseJson>
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
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.
|
||||
*/
|
||||
interface IdentityApi {
|
||||
|
||||
/**
|
||||
* Make request to get an access token.
|
||||
*/
|
||||
@POST
|
||||
@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",
|
||||
@Field(value = "username") email: String,
|
||||
@Header(value = "auth-email") authEmail: String = email.base64UrlEncode(),
|
||||
@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>
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
/**
|
||||
* Models high level auth state for the application.
|
||||
*/
|
||||
sealed class AuthState {
|
||||
|
||||
/**
|
||||
* Auth state is unknown.
|
||||
*/
|
||||
data object Uninitialized : AuthState()
|
||||
|
||||
/**
|
||||
* User is unauthenticated. Said another way, the app has no access token.
|
||||
*/
|
||||
data object Unauthenticated : AuthState()
|
||||
|
||||
/**
|
||||
* User is authenticated with the given access token.
|
||||
*/
|
||||
data class Authenticated(val accessToken: String) : AuthState()
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models json response of the get token request.
|
||||
*
|
||||
* @param accessToken the access token.
|
||||
*/
|
||||
@Serializable
|
||||
data class GetTokenResponseJson(
|
||||
@SerialName("access_token")
|
||||
val accessToken: String,
|
||||
)
|
|
@ -0,0 +1,19 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
/**
|
||||
* Models result of logging in.
|
||||
*
|
||||
* TODO: Add more detail to these cases to expose server error messages (BIT-320)
|
||||
*/
|
||||
sealed class LoginResult {
|
||||
|
||||
/**
|
||||
* Login succeeded.
|
||||
*/
|
||||
data object Success : LoginResult()
|
||||
|
||||
/**
|
||||
* There was an error logging in.
|
||||
*/
|
||||
data object Error : LoginResult()
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body for pre login.
|
||||
*/
|
||||
@Serializable
|
||||
data class PreLoginRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
)
|
|
@ -0,0 +1,20 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Response body for pre login.
|
||||
*/
|
||||
@Serializable
|
||||
data class PreLoginResponseJson(
|
||||
// TODO parse this property as an enum (BIT-329)
|
||||
@SerialName("kdf")
|
||||
val kdf: Int,
|
||||
@SerialName("kdfIterations")
|
||||
val kdfIterations: UInt,
|
||||
@SerialName("kdfMemory")
|
||||
val kdfMemory: Int? = null,
|
||||
@SerialName("kdfParallelism")
|
||||
val kdfParallelism: Int? = null,
|
||||
)
|
|
@ -0,0 +1,24 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Provides an API for observing an modifying authentication state.
|
||||
*/
|
||||
interface AuthRepository {
|
||||
/**
|
||||
* Models the current auth state.
|
||||
*/
|
||||
val authStateFlow: StateFlow<AuthState>
|
||||
|
||||
/**
|
||||
* Attempt to login with the given email and password. Updated access token will be reflected
|
||||
* in [authStateFlow].
|
||||
*/
|
||||
suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
): LoginResult
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
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.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Default implementation of [AuthRepository].
|
||||
*/
|
||||
@Singleton
|
||||
class AuthRepositoryImpl @Inject constructor(
|
||||
private val accountsApi: AccountsApi,
|
||||
private val identityApi: IdentityApi,
|
||||
private val bitwardenSdkClient: Client,
|
||||
) : AuthRepository {
|
||||
|
||||
private val mutableAccessTokenFlow = MutableStateFlow<AuthState>(AuthState.Unauthenticated)
|
||||
override val authStateFlow: StateFlow<AuthState> = mutableAccessTokenFlow.asStateFlow()
|
||||
|
||||
/**
|
||||
* Attempt to login with the given email.
|
||||
*/
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
): LoginResult = accountsApi
|
||||
.preLogin(PreLoginRequestJson(email))
|
||||
.flatMap {
|
||||
// TODO: Use KDF enum from pre login correctly (BIT-329)
|
||||
val passwordHash = bitwardenSdkClient
|
||||
.auth()
|
||||
.hashPassword(
|
||||
email = email,
|
||||
password = password,
|
||||
kdfParams = Kdf.Pbkdf2(it.kdfIterations),
|
||||
)
|
||||
identityApi.getToken(
|
||||
email = email,
|
||||
passwordHash = passwordHash,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = {
|
||||
// TODO: Add more detail to these cases to expose server error messages (BIT-320)
|
||||
LoginResult.Error
|
||||
},
|
||||
onSuccess = {
|
||||
mutableAccessTokenFlow.value = AuthState.Authenticated(it.accessToken)
|
||||
LoginResult.Success
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.di
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
/**
|
||||
* Provides repositories in the auth package.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class RepositoryModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindsAuthRepository(authRepositoryImpl: AuthRepositoryImpl): AuthRepository
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
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 dagger.Module
|
||||
|
@ -12,6 +14,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
|
@ -24,9 +27,15 @@ object NetworkModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConfigApiService(retrofit: Retrofit): ConfigApi {
|
||||
return retrofit.create(ConfigApi::class.java)
|
||||
}
|
||||
fun providesAccountsApiService(retrofit: Retrofit): AccountsApi = retrofit.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesConfigApiService(retrofit: Retrofit): ConfigApi = retrofit.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIdentityApiService(retrofit: Retrofit): IdentityApi = retrofit.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
@ -42,14 +51,27 @@ object NetworkModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
||||
fun provideRetrofit(
|
||||
okHttpClient: OkHttpClient,
|
||||
json: Json,
|
||||
): Retrofit {
|
||||
val contentType = "application/json".toMediaType()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl("https://api.bitwarden.com")
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(Json.asConverterFactory(contentType))
|
||||
.addConverterFactory(json.asConverterFactory(contentType))
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesJson(): Json = Json {
|
||||
|
||||
// If there are keys returned by the server not modeled by a serializable class,
|
||||
// ignore them.
|
||||
// This makes additive server changes non-breaking.
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
import java.util.Base64
|
||||
|
||||
/**
|
||||
* Base 64 encode the string as well as make special modifications required by the backend:
|
||||
*
|
||||
* - replace all "+" with "-"
|
||||
* - replace all "/" with "_"
|
||||
* - replace all "=" with ""
|
||||
*/
|
||||
fun String.base64UrlEncode(): String =
|
||||
Base64.getEncoder()
|
||||
.encodeToString(toByteArray())
|
||||
.replace("+", "-")
|
||||
.replace("/", "_")
|
||||
.replace("=", "")
|
|
@ -3,11 +3,14 @@ package com.x8bit.bitwarden.ui.auth.feature.login
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -18,12 +21,13 @@ private const val KEY_STATE = "state"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<LoginState, LoginEvent, LoginAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: LoginState(
|
||||
emailAddress = LoginArgs(savedStateHandle).emailAddress,
|
||||
isLoginButtonEnabled = false,
|
||||
isLoginButtonEnabled = true,
|
||||
passwordInput = "",
|
||||
),
|
||||
) {
|
||||
|
@ -45,7 +49,19 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleLoginButtonClicked() {
|
||||
// TODO BIT-133 make login request and allow user to login
|
||||
viewModelScope.launch {
|
||||
// TODO: show progress here BIT-320
|
||||
val result = authRepository.login(
|
||||
email = mutableStateFlow.value.emailAddress,
|
||||
password = mutableStateFlow.value.passwordInput,
|
||||
)
|
||||
when (result) {
|
||||
// TODO: show an error here BIT-320
|
||||
LoginResult.Error -> Unit
|
||||
// No action required on success, root nav will navigate to logged in state
|
||||
LoginResult.Success -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNotYouButtonClicked() {
|
||||
|
|
|
@ -3,13 +3,13 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
|||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -20,8 +20,9 @@ private const val KEY_NAV_DESTINATION = "nav_state"
|
|||
*/
|
||||
@HiltViewModel
|
||||
class RootNavViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<RootNavState, Unit, Unit>(
|
||||
) : BaseViewModel<RootNavState, Unit, RootNavAction>(
|
||||
initialState = RootNavState.Splash,
|
||||
) {
|
||||
|
||||
|
@ -39,14 +40,25 @@ class RootNavViewModel @Inject constructor(
|
|||
stateFlow
|
||||
.onEach { savedRootNavState = it }
|
||||
.launchIn(viewModelScope)
|
||||
viewModelScope.launch {
|
||||
@Suppress("MagicNumber")
|
||||
delay(1000)
|
||||
mutableStateFlow.update { RootNavState.Auth }
|
||||
authRepository
|
||||
.authStateFlow
|
||||
.onEach { trySendAction(RootNavAction.AuthStateUpdated(it)) }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: RootNavAction) {
|
||||
when (action) {
|
||||
is RootNavAction.AuthStateUpdated -> handleAuthStateUpdated(action)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: Unit) = Unit
|
||||
private fun handleAuthStateUpdated(action: RootNavAction.AuthStateUpdated) {
|
||||
when (action.newState) {
|
||||
is AuthState.Authenticated -> mutableStateFlow.update { RootNavState.VaultUnlocked }
|
||||
is AuthState.Unauthenticated -> mutableStateFlow.update { RootNavState.Auth }
|
||||
is AuthState.Uninitialized -> mutableStateFlow.update { RootNavState.Splash }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,3 +83,14 @@ sealed class RootNavState : Parcelable {
|
|||
@Parcelize
|
||||
data object VaultUnlocked : RootNavState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models root level navigation actions.
|
||||
*/
|
||||
sealed class RootNavAction {
|
||||
|
||||
/**
|
||||
* Auth state in the repository layer changed.
|
||||
*/
|
||||
data class AuthStateUpdated(val newState: AuthState) : RootNavAction()
|
||||
}
|
||||
|
|
|
@ -2,7 +2,12 @@ package com.x8bit.bitwarden.ui.auth.feature.login
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -15,7 +20,10 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = LoginViewModel(savedStateHandle)
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
|
@ -33,26 +41,56 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
"state" to expectedState,
|
||||
),
|
||||
)
|
||||
val viewModel = LoginViewModel(handle)
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
savedStateHandle = handle,
|
||||
)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedState, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LoginButtonClick should do nothing`() = runTest {
|
||||
fun `LoginButtonClick login returns error should do nothing`() = runTest {
|
||||
// TODO: handle and display errors (BIT-320)
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
coEvery { login(email = "test@gmail.com", password = "") } returns LoginResult.Error
|
||||
}
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LoginAction.LoginButtonClick)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LoginButtonClick login returns success should do nothing`() = runTest {
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
coEvery { login("test@gmail.com", "") } returns LoginResult.Success
|
||||
}
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LoginAction.LoginButtonClick)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
coVerify {
|
||||
authRepository.login(email = "test@gmail.com", password = "")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SingleSignOnClick should do nothing`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
|
@ -64,6 +102,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `NotYouButtonClick should emit NavigateToLanding`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
|
@ -79,6 +118,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
fun `PasswordInputChanged should update password input`() = runTest {
|
||||
val input = "input"
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk(),
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
|
@ -94,7 +134,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
private val DEFAULT_STATE = LoginState(
|
||||
emailAddress = "test@gmail.com",
|
||||
passwordInput = "",
|
||||
isLoginButtonEnabled = false,
|
||||
isLoginButtonEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,64 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState.Authenticated
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState.Unauthenticated
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class RootNavViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial state should be splash`() {
|
||||
val viewModel = RootNavViewModel(SavedStateHandle())
|
||||
assertEquals(viewModel.stateFlow.value, RootNavState.Splash)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be the state in savedStateHandle`() {
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
every { this@mockk.authStateFlow } returns MutableStateFlow(mockk<Authenticated>())
|
||||
}
|
||||
val handle = SavedStateHandle(mapOf(("nav_state" to RootNavState.VaultUnlocked)))
|
||||
val viewModel = RootNavViewModel(handle)
|
||||
assertEquals(viewModel.stateFlow.value, RootNavState.VaultUnlocked)
|
||||
val viewModel = RootNavViewModel(
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = handle,
|
||||
)
|
||||
assertEquals(RootNavState.VaultUnlocked, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state should move from splash to auth`() = runTest {
|
||||
val viewModel = RootNavViewModel(SavedStateHandle())
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(awaitItem(), RootNavState.Splash)
|
||||
assertEquals(awaitItem(), RootNavState.Auth)
|
||||
}
|
||||
fun `when auth state is Uninitialized nav state should be Splash`() {
|
||||
val viewModel = RootNavViewModel(
|
||||
authRepository = mockk {
|
||||
every { this@mockk.authStateFlow } returns MutableStateFlow(AuthState.Uninitialized)
|
||||
},
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
)
|
||||
assertEquals(RootNavState.Splash, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when auth state is Authenticated nav state should be VaultUnlocked`() {
|
||||
val authRepository = mockk<AuthRepository> {
|
||||
every { this@mockk.authStateFlow } returns MutableStateFlow(mockk<Authenticated>())
|
||||
}
|
||||
val viewModel = RootNavViewModel(
|
||||
authRepository = authRepository,
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
)
|
||||
assertEquals(RootNavState.VaultUnlocked, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when auth state is Unauthenticated nav state should be Auth`() = runTest {
|
||||
val viewModel = RootNavViewModel(
|
||||
authRepository = mockk {
|
||||
every { this@mockk.authStateFlow } returns MutableStateFlow(Unauthenticated)
|
||||
},
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
)
|
||||
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue