diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AccountsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AccountsApi.kt new file mode 100644 index 0000000000..c7fbc8b4cc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AccountsApi.kt @@ -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 +} 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 new file mode 100644 index 0000000000..26f0bac57a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt @@ -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 +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthState.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthState.kt new file mode 100644 index 0000000000..e8f11eafae --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthState.kt @@ -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() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/GetTokenResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/GetTokenResponseJson.kt new file mode 100644 index 0000000000..3a5cfee810 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/GetTokenResponseJson.kt @@ -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, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/LoginResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/LoginResult.kt new file mode 100644 index 0000000000..78ff14bf1f --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/LoginResult.kt @@ -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() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginRequestJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginRequestJson.kt new file mode 100644 index 0000000000..71f6f5b83d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginRequestJson.kt @@ -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, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt new file mode 100644 index 0000000000..5c040bf410 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/PreLoginResponseJson.kt @@ -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, +) 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 new file mode 100644 index 0000000000..c7ef5263bc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -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 + + /** + * 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 +} 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 new file mode 100644 index 0000000000..dba73a0d32 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -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.Unauthenticated) + override val authStateFlow: StateFlow = 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 + }, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/RepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/RepositoryModule.kt new file mode 100644 index 0000000000..e3efbbe5eb --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/RepositoryModule.kt @@ -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 +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/NetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/NetworkModule.kt index 4f9b313eb1..3ab242f242 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/NetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/NetworkModule.kt @@ -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 + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtils.kt new file mode 100644 index 0000000000..3fe0544565 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/util/NetworkUtils.kt @@ -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("=", "") diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt index 8ad847a0f5..62fbf0f418 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt @@ -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( 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() { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index a8f817cd32..ffce750b97 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -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( +) : BaseViewModel( 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() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt index 3e9799d1da..5bd2beb10b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt @@ -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 { + 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 { + 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, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index 2bd014f643..9d6cb166e6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -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 { + every { this@mockk.authStateFlow } returns MutableStateFlow(mockk()) + } 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 { + every { this@mockk.authStateFlow } returns MutableStateFlow(mockk()) } + 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) } }