From 6cbfff254ca0b8d4745cd8e2144fd840e30dedc0 Mon Sep 17 00:00:00 2001 From: Caleb Derosier <125901828+caleb-livefront@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:29:46 -0700 Subject: [PATCH] BIT-808: Conditionally show log in with device on login (#681) --- .../auth/datasource/network/api/DevicesApi.kt | 16 +++++++ .../network/di/AuthNetworkModule.kt | 10 ++++ .../network/service/DevicesService.kt | 14 ++++++ .../network/service/DevicesServiceImpl.kt | 16 +++++++ .../data/auth/repository/AuthRepository.kt | 6 +++ .../auth/repository/AuthRepositoryImpl.kt | 18 +++++-- .../repository/di/AuthRepositoryModule.kt | 4 ++ .../repository/model/KnownDeviceResult.kt | 16 +++++++ .../ui/auth/feature/login/LoginScreen.kt | 21 ++++---- .../ui/auth/feature/login/LoginViewModel.kt | 48 ++++++++++++++++++- .../network/service/DevicesServiceTest.kt | 33 +++++++++++++ .../auth/repository/AuthRepositoryTest.kt | 33 +++++++++++++ .../ui/auth/feature/login/LoginScreenTest.kt | 12 +++++ .../auth/feature/login/LoginViewModelTest.kt | 32 +++++++++++++ 14 files changed, 265 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/DevicesApi.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesService.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/KnownDeviceResult.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/DevicesApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/DevicesApi.kt new file mode 100644 index 0000000000..be36531136 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/DevicesApi.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.api + +import retrofit2.http.GET +import retrofit2.http.Header + +/** + * Defines raw calls under the /devices API. + */ +interface DevicesApi { + + @GET("/devices/knowndevice") + suspend fun getIsKnownDevice( + @Header(value = "X-Request-Email") emailAddress: String, + @Header(value = "X-Device-Identifier") deviceId: String, + ): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt index cbab1585cc..569efc77bf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt @@ -2,6 +2,8 @@ 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.DevicesService +import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesServiceImpl import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedServiceImpl import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService @@ -33,6 +35,14 @@ object AuthNetworkModule { json = json, ) + @Provides + @Singleton + fun providesDevicesService( + retrofits: Retrofits, + ): DevicesService = DevicesServiceImpl( + devicesApi = retrofits.unauthenticatedApiRetrofit.create(), + ) + @Provides @Singleton fun providesIdentityService( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesService.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesService.kt new file mode 100644 index 0000000000..bf9889a1d8 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesService.kt @@ -0,0 +1,14 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.service + +/** + * Provides an API for interacting with the /devices endpoints. + */ +interface DevicesService { + /** + * Check whether this device is known (and thus whether Login with Device is available). + */ + suspend fun getIsKnownDevice( + emailAddress: String, + deviceId: String, + ): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt new file mode 100644 index 0000000000..eddf700cd4 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceImpl.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.service + +import com.x8bit.bitwarden.data.auth.datasource.network.api.DevicesApi +import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode + +class DevicesServiceImpl( + private val devicesApi: DevicesApi, +) : DevicesService { + override suspend fun getIsKnownDevice( + emailAddress: String, + deviceId: String, + ): Result = devicesApi.getIsKnownDevice( + emailAddress = emailAddress.base64UrlEncode(), + deviceId = deviceId, + ) +} 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 d27ebeb88d..c075e6a8d6 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult +import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult @@ -93,6 +94,11 @@ interface AuthRepository : AuthenticatorProvider { */ fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) + /** + * Get a [Boolean] indicating whether this is a known device. + */ + suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult + /** * Attempts to get the number of times the given [password] has been breached. */ 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 59df6a35f8..75e2f88d9c 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 @@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenRespon 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.service.AccountsService +import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource @@ -19,6 +20,7 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult +import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult @@ -42,7 +44,6 @@ import com.x8bit.bitwarden.data.platform.util.flatMap import com.x8bit.bitwarden.data.vault.repository.VaultRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -58,8 +59,9 @@ import javax.inject.Singleton */ @Suppress("LongParameterList", "TooManyFunctions") @Singleton -class AuthRepositoryImpl constructor( +class AuthRepositoryImpl( private val accountsService: AccountsService, + private val devicesService: DevicesService, private val haveIBeenPwnedService: HaveIBeenPwnedService, private val identityService: IdentityService, private val authSdkSource: AuthSdkSource, @@ -101,7 +103,6 @@ class AuthRepositoryImpl constructor( initialValue = AuthState.Uninitialized, ) - @OptIn(ExperimentalCoroutinesApi::class) override val userStateFlow: StateFlow = combine( authDiskSource.userStateFlow, authDiskSource.userOrganizationsListFlow, @@ -377,6 +378,17 @@ class AuthRepositoryImpl constructor( mutableCaptchaTokenFlow.tryEmit(tokenResult) } + override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult = + devicesService + .getIsKnownDevice( + emailAddress = emailAddress, + deviceId = authDiskSource.uniqueAppId, + ) + .fold( + onFailure = { KnownDeviceResult.Error }, + onSuccess = { KnownDeviceResult.Success(it) }, + ) + override suspend fun getPasswordBreachCount(password: String): BreachCountResult = haveIBeenPwnedService .getPasswordBreachCount(password) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt index 04d48ad2a5..7a8a6bcf4e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.di import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService +import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource @@ -27,8 +28,10 @@ object AuthRepositoryModule { @Provides @Singleton + @Suppress("LongParameterList") fun providesAuthRepository( accountsService: AccountsService, + devicesService: DevicesService, identityService: IdentityService, haveIBeenPwnedService: HaveIBeenPwnedService, authSdkSource: AuthSdkSource, @@ -40,6 +43,7 @@ object AuthRepositoryModule { userLogoutManager: UserLogoutManager, ): AuthRepository = AuthRepositoryImpl( accountsService = accountsService, + devicesService = devicesService, identityService = identityService, authSdkSource = authSdkSource, authDiskSource = authDiskSource, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/KnownDeviceResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/KnownDeviceResult.kt new file mode 100644 index 0000000000..05ae8796ea --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/KnownDeviceResult.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.auth.repository.model + +/** + * Models result of checking whether this is a known device. + */ +sealed class KnownDeviceResult { + /** + * Contains a [Boolean] indicating whether this is a known device. + */ + data class Success(val isKnownDevice: Boolean) : KnownDeviceResult() + + /** + * There was an error determining if this is a known device. + */ + data object Error : KnownDeviceResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt index 1e88035d8b..a888484cd4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt @@ -244,17 +244,18 @@ private fun LoginScreenContent( Spacer(modifier = Modifier.height(12.dp)) - // TODO BIT-808: Hide button for first-time users - BitwardenOutlinedButtonWithIcon( - label = stringResource(id = R.string.log_in_with_device), - icon = painterResource(id = R.drawable.ic_device), - onClick = onLoginWithDeviceClick, - modifier = Modifier - .semantics { testTag = "LogInWithAnotherDeviceButton" } - .fillMaxWidth(), - ) + if (state.shouldShowLoginWithDevice) { + BitwardenOutlinedButtonWithIcon( + label = stringResource(id = R.string.log_in_with_device), + icon = painterResource(id = R.drawable.ic_device), + onClick = onLoginWithDeviceClick, + modifier = Modifier + .semantics { testTag = "LogInWithAnotherDeviceButton" } + .fillMaxWidth(), + ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) + } BitwardenOutlinedButtonWithIcon( label = stringResource(id = R.string.log_in_sso), 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 e976802756..9ee48bae9f 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 @@ -8,6 +8,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha @@ -45,10 +46,11 @@ class LoginViewModel @Inject constructor( isLoginButtonEnabled = false, passwordInput = "", environmentLabel = environmentRepository.environment.label, - loadingDialogState = LoadingDialogState.Hidden, + loadingDialogState = LoadingDialogState.Shown(R.string.loading.asText()), errorDialogState = BasicDialogState.Hidden, captchaToken = LoginArgs(savedStateHandle).captchaToken, accountSummaries = authRepository.userStateFlow.value?.toAccountSummaries().orEmpty(), + shouldShowLoginWithDevice = false, ), ) { @@ -66,6 +68,14 @@ class LoginViewModel @Inject constructor( ) } .launchIn(viewModelScope) + + viewModelScope.launch { + trySendAction( + LoginAction.Internal.ReceiveKnownDeviceResult( + knownDeviceResult = authRepository.getIsKnownDevice(state.emailAddress), + ), + ) + } } override fun handleAction(action: LoginAction) { @@ -89,6 +99,10 @@ class LoginViewModel @Inject constructor( is LoginAction.Internal.ReceiveLoginResult -> { handleReceiveLoginResult(action = action) } + + is LoginAction.Internal.ReceiveKnownDeviceResult -> { + handleKnownDeviceResultReceived(action) + } } } @@ -109,6 +123,30 @@ class LoginViewModel @Inject constructor( authRepository.switchAccount(userId = action.accountSummary.userId) } + private fun handleKnownDeviceResultReceived( + action: LoginAction.Internal.ReceiveKnownDeviceResult, + ) { + when (action.knownDeviceResult) { + is KnownDeviceResult.Success -> { + mutableStateFlow.update { + it.copy( + loadingDialogState = LoadingDialogState.Hidden, + shouldShowLoginWithDevice = action.knownDeviceResult.isKnownDevice, + ) + } + } + + is KnownDeviceResult.Error -> { + mutableStateFlow.update { + it.copy( + loadingDialogState = LoadingDialogState.Hidden, + shouldShowLoginWithDevice = false, + ) + } + } + } + } + private fun handleReceiveLoginResult(action: LoginAction.Internal.ReceiveLoginResult) { when (val loginResult = action.loginResult) { is LoginResult.CaptchaRequired -> { @@ -235,6 +273,7 @@ data class LoginState( val loadingDialogState: LoadingDialogState, val errorDialogState: BasicDialogState, val accountSummaries: List, + val shouldShowLoginWithDevice: Boolean, ) : Parcelable /** @@ -352,6 +391,13 @@ sealed class LoginAction { val tokenResult: CaptchaCallbackTokenResult, ) : Internal() + /** + * Indicates that a [KnownDeviceResult] has been received and state should be updated. + */ + data class ReceiveKnownDeviceResult( + val knownDeviceResult: KnownDeviceResult, + ) : Internal() + /** * Indicates a login result has been received. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceTest.kt new file mode 100644 index 0000000000..40f929df3e --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/DevicesServiceTest.kt @@ -0,0 +1,33 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.service + +import com.x8bit.bitwarden.data.auth.datasource.network.api.DevicesApi +import com.x8bit.bitwarden.data.platform.base.BaseServiceTest +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import retrofit2.create + +class DevicesServiceTest : BaseServiceTest() { + + private val devicesApi: DevicesApi = retrofit.create() + private val service = DevicesServiceImpl( + devicesApi = devicesApi, + ) + + @Test + fun `getIsKnownDevice when request response is Failure should return Failure`() = runTest { + val response = MockResponse().setResponseCode(400) + server.enqueue(response) + val actual = service.getIsKnownDevice("email", "id") + assertTrue(actual.isFailure) + } + + @Test + fun `getIsKnownDevice when request response is Success should return Success`() = runTest { + val response = MockResponse().setBody("false").setResponseCode(200) + server.enqueue(response) + val actual = service.getIsKnownDevice("email", "id") + assertTrue(actual.isSuccess) + } +} 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 dc7f6c236b..8b477a5ee4 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 @@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenRespon 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.service.AccountsService +import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource @@ -28,6 +29,7 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult +import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult @@ -76,6 +78,7 @@ class AuthRepositoryTest { private val dispatcherManager: DispatcherManager = FakeDispatcherManager() private val accountsService: AccountsService = mockk() + private val devicesService: DevicesService = mockk() private val identityService: IdentityService = mockk() private val haveIBeenPwnedService: HaveIBeenPwnedService = mockk() private val mutableVaultStateFlow = MutableStateFlow(VAULT_STATE) @@ -127,6 +130,7 @@ class AuthRepositoryTest { private val repository = AuthRepositoryImpl( accountsService = accountsService, + devicesService = devicesService, identityService = identityService, haveIBeenPwnedService = haveIBeenPwnedService, authSdkSource = authSdkSource, @@ -1173,6 +1177,35 @@ class AuthRepositoryTest { ) } + @Test + fun `getIsKnownDevice should return failure when service returns failure`() = runTest { + coEvery { + devicesService.getIsKnownDevice(EMAIL, UNIQUE_APP_ID) + } returns Throwable("Fail").asFailure() + + val result = repository.getIsKnownDevice(EMAIL) + + coVerify(exactly = 1) { + devicesService.getIsKnownDevice(EMAIL, UNIQUE_APP_ID) + } + assertEquals(KnownDeviceResult.Error, result) + } + + @Test + fun `getIsKnownDevice should return success when service returns success`() = runTest { + val isKnownDevice = true + coEvery { + devicesService.getIsKnownDevice(EMAIL, UNIQUE_APP_ID) + } returns isKnownDevice.asSuccess() + + val result = repository.getIsKnownDevice(EMAIL) + + coVerify(exactly = 1) { + devicesService.getIsKnownDevice(EMAIL, UNIQUE_APP_ID) + } + assertEquals(KnownDeviceResult.Success(isKnownDevice), result) + } + @Test fun `getPasswordBreachCount should return failure when service returns failure`() = runTest { val password = "password" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt index af0029724c..6a797f6b98 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt @@ -203,6 +203,17 @@ class LoginScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() } + @Test + fun `log in with device button visibility should update according to state`() { + val buttonText = "Log in with device" + composeTestRule.onNodeWithText(buttonText).assertDoesNotExist() + + mutableStateFlow.update { + it.copy(shouldShowLoginWithDevice = true) + } + composeTestRule.onNodeWithText(buttonText).assertIsDisplayed() + } + @Test fun `close button click should send CloseButtonClick action`() { composeTestRule.onNodeWithContentDescription("Close").performClick() @@ -302,4 +313,5 @@ private val DEFAULT_STATE = loadingDialogState = LoadingDialogState.Hidden, errorDialogState = BasicDialogState.Hidden, accountSummaries = emptyList(), + shouldShowLoginWithDevice = false, ) 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 316ad6c3d6..5b53f2ee58 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 @@ -6,6 +6,7 @@ import app.cash.turbine.test import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult @@ -45,6 +46,9 @@ class LoginViewModelTest : BaseViewModelTest() { bufferedMutableSharedFlow() private val mutableUserStateFlow = MutableStateFlow(null) private val authRepository: AuthRepository = mockk(relaxed = true) { + coEvery { + getIsKnownDevice("test@gmail.com") + } returns KnownDeviceResult.Success(false) every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow every { userStateFlow } returns mutableUserStateFlow every { logout(any()) } just runs @@ -150,6 +154,33 @@ class LoginViewModelTest : BaseViewModelTest() { } } + @Test + fun `should set shouldShowLoginWithDevice when isKnownDevice returns true`() = runTest { + val expectedState = DEFAULT_STATE.copy( + shouldShowLoginWithDevice = true, + ) + coEvery { + authRepository.getIsKnownDevice("test@gmail.com") + } returns KnownDeviceResult.Success(true) + val viewModel = createViewModel() + + viewModel.stateFlow.test { + assertEquals(expectedState, awaitItem()) + } + } + + @Test + fun `should have default state when isKnownDevice returns error`() = runTest { + coEvery { + authRepository.getIsKnownDevice("test@gmail.com") + } returns KnownDeviceResult.Error + val viewModel = createViewModel() + + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + } + } + @Suppress("MaxLineLength") @Test fun `on AddAccountClick should send NavigateBack`() = runTest { @@ -434,6 +465,7 @@ class LoginViewModelTest : BaseViewModelTest() { errorDialogState = BasicDialogState.Hidden, captchaToken = null, accountSummaries = emptyList(), + shouldShowLoginWithDevice = false, ) private const val LOGIN_RESULT_PATH =