mirror of
https://github.com/bitwarden/android.git
synced 2026-06-02 11:12:00 -05:00
BIT-543: Add Remember Me functionality to Landing Screen (#104)
Co-authored-by: Brian Yencho <brian@livefront.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AuthDiskSourceTest {
|
||||
private val fakeSharedPreferences = FakeSharedPreferences()
|
||||
|
||||
private val authDiskSource = AuthDiskSourceImpl(
|
||||
sharedPreferences = fakeSharedPreferences,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `rememberedEmailAddress should pull from and update SharedPreferences`() {
|
||||
val rememberedEmailKey = "bwPreferencesStorage:rememberedEmail"
|
||||
|
||||
// Shared preferences and the repository start with the same value.
|
||||
assertNull(authDiskSource.rememberedEmailAddress)
|
||||
assertNull(fakeSharedPreferences.getString(rememberedEmailKey, null))
|
||||
|
||||
// Updating the repository updates shared preferences
|
||||
authDiskSource.rememberedEmailAddress = "remembered@gmail.com"
|
||||
assertEquals(
|
||||
"remembered@gmail.com",
|
||||
fakeSharedPreferences.getString(rememberedEmailKey, null),
|
||||
)
|
||||
|
||||
// Update SharedPreferences updates the repository
|
||||
fakeSharedPreferences.edit().putString(rememberedEmailKey, null).apply()
|
||||
assertNull(authDiskSource.rememberedEmailAddress)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
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
|
||||
@@ -19,6 +20,7 @@ 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.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -27,6 +29,7 @@ class AuthRepositoryTest {
|
||||
private val accountsService: AccountsService = mockk()
|
||||
private val identityService: IdentityService = mockk()
|
||||
private val authInterceptor = mockk<AuthTokenInterceptor>()
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val mockBitwardenSdk = mockk<Client> {
|
||||
coEvery {
|
||||
auth().hashPassword(
|
||||
@@ -41,6 +44,7 @@ class AuthRepositoryTest {
|
||||
accountsService = accountsService,
|
||||
identityService = identityService,
|
||||
bitwardenSdkClient = mockBitwardenSdk,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
authTokenInterceptor = authInterceptor,
|
||||
)
|
||||
|
||||
@@ -49,6 +53,21 @@ class AuthRepositoryTest {
|
||||
clearMocks(identityService, accountsService, authInterceptor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rememberedEmailAddress should pull from and update AuthDiskSource`() {
|
||||
// AuthDiskSource and the repository start with the same value.
|
||||
assertNull(repository.rememberedEmailAddress)
|
||||
assertNull(fakeAuthDiskSource.rememberedEmailAddress)
|
||||
|
||||
// Updating the repository updates AuthDiskSource
|
||||
repository.rememberedEmailAddress = "remembered@gmail.com"
|
||||
assertEquals("remembered@gmail.com", fakeAuthDiskSource.rememberedEmailAddress)
|
||||
|
||||
// Updating AuthDiskSource updates the repository
|
||||
fakeAuthDiskSource.rememberedEmailAddress = null
|
||||
assertNull(repository.rememberedEmailAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login when pre login fails should return Error with no message`() = runTest {
|
||||
coEvery {
|
||||
@@ -197,3 +216,7 @@ class AuthRepositoryTest {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeAuthDiskSource : AuthDiskSource {
|
||||
override var rememberedEmailAddress: String? = null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.x8bit.bitwarden.data.platform.base
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
||||
/**
|
||||
* A faked implementation of [SharedPreferences] that is backed by an internal, memory-based map.
|
||||
*/
|
||||
class FakeSharedPreferences : SharedPreferences {
|
||||
private val sharedPreferences: MutableMap<String, Any?> = mutableMapOf()
|
||||
|
||||
override fun contains(key: String): Boolean =
|
||||
sharedPreferences.containsKey(key)
|
||||
|
||||
override fun edit(): SharedPreferences.Editor = Editor()
|
||||
|
||||
override fun getAll(): Map<String, *> = sharedPreferences
|
||||
|
||||
override fun getBoolean(key: String, defaultValue: Boolean): Boolean =
|
||||
getValue(key, defaultValue)
|
||||
|
||||
override fun getFloat(key: String, defaultValue: Float): Float =
|
||||
getValue(key, defaultValue)
|
||||
|
||||
override fun getInt(key: String, defaultValue: Int): Int =
|
||||
getValue(key, defaultValue)
|
||||
|
||||
override fun getLong(key: String, defaultValue: Long): Long =
|
||||
getValue(key, defaultValue)
|
||||
|
||||
override fun getString(key: String, defaultValue: String?): String? =
|
||||
getValue(key, defaultValue)
|
||||
|
||||
override fun getStringSet(key: String, defaultValue: Set<String>?): Set<String>? =
|
||||
getValue(key, defaultValue)
|
||||
|
||||
override fun registerOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
) {
|
||||
throw NotImplementedError(
|
||||
"registerOnSharedPreferenceChangeListener is not currently implemented.",
|
||||
)
|
||||
}
|
||||
|
||||
override fun unregisterOnSharedPreferenceChangeListener(
|
||||
listener: SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
) {
|
||||
throw NotImplementedError(
|
||||
"unregisterOnSharedPreferenceChangeListener is not currently implemented.",
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <reified T> getValue(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
): T = sharedPreferences[key] as? T ?: defaultValue
|
||||
|
||||
inner class Editor : SharedPreferences.Editor {
|
||||
private val pendingSharedPreferences = sharedPreferences.toMutableMap()
|
||||
|
||||
override fun apply() {
|
||||
sharedPreferences.apply {
|
||||
clear()
|
||||
putAll(pendingSharedPreferences)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear(): SharedPreferences.Editor =
|
||||
apply { pendingSharedPreferences.clear() }
|
||||
|
||||
override fun commit(): Boolean {
|
||||
apply()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor =
|
||||
putValue(key, value)
|
||||
|
||||
override fun putFloat(key: String, value: Float): SharedPreferences.Editor =
|
||||
putValue(key, value)
|
||||
|
||||
override fun putInt(key: String, value: Int): SharedPreferences.Editor =
|
||||
putValue(key, value)
|
||||
|
||||
override fun putLong(key: String, value: Long): SharedPreferences.Editor =
|
||||
putValue(key, value)
|
||||
|
||||
override fun putString(key: String, value: String?): SharedPreferences.Editor =
|
||||
putValue(key, value)
|
||||
|
||||
override fun putStringSet(key: String, value: Set<String>?): SharedPreferences.Editor =
|
||||
putValue(key, value)
|
||||
|
||||
override fun remove(key: String): SharedPreferences.Editor =
|
||||
apply { pendingSharedPreferences.remove(key) }
|
||||
|
||||
private inline fun <reified T> putValue(
|
||||
key: String,
|
||||
value: T,
|
||||
): SharedPreferences.Editor = apply { pendingSharedPreferences[key] = value }
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,37 @@ package com.x8bit.bitwarden.ui.auth.feature.landing
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class LandingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = LandingViewModel(SavedStateHandle())
|
||||
fun `initial state should be correct when there is no remembered email`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when there is a remembered email`() = runTest {
|
||||
val rememberedEmail = "remembered@gmail.com"
|
||||
val viewModel = createViewModel(rememberedEmail = rememberedEmail)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
emailInput = rememberedEmail,
|
||||
isContinueButtonEnabled = true,
|
||||
isRememberMeEnabled = true,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should pull from saved state handle when present`() = runTest {
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
@@ -25,7 +42,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
isRememberMeEnabled = true,
|
||||
)
|
||||
val handle = SavedStateHandle(mapOf("state" to expectedState))
|
||||
val viewModel = LandingViewModel(handle)
|
||||
val viewModel = createViewModel(savedStateHandle = handle)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedState, awaitItem())
|
||||
}
|
||||
@@ -33,7 +50,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick should emit NavigateToLogin`() = runTest {
|
||||
val viewModel = LandingViewModel(SavedStateHandle())
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(LandingAction.EmailInputChanged("input"))
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LandingAction.ContinueButtonClick)
|
||||
@@ -46,7 +63,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick with empty input should do nothing`() = runTest {
|
||||
val viewModel = LandingViewModel(SavedStateHandle())
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LandingAction.ContinueButtonClick)
|
||||
}
|
||||
@@ -54,7 +71,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `CreateAccountClick should emit NavigateToCreateAccount`() = runTest {
|
||||
val viewModel = LandingViewModel(SavedStateHandle())
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LandingAction.CreateAccountClick)
|
||||
assertEquals(
|
||||
@@ -66,7 +83,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `RememberMeToggle should update value of isRememberMeToggled`() = runTest {
|
||||
val viewModel = LandingViewModel(SavedStateHandle())
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LandingAction.RememberMeToggle(true))
|
||||
assertEquals(
|
||||
@@ -79,7 +96,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `EmailInputUpdated should update value of email input and continue button state`() =
|
||||
runTest {
|
||||
val viewModel = LandingViewModel(SavedStateHandle())
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
// Ignore initial state
|
||||
awaitItem()
|
||||
@@ -109,7 +126,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `RegionOptionSelect should update value of selected region`() = runTest {
|
||||
val inputRegion = LandingState.RegionOption.BITWARDEN_EU
|
||||
val viewModel = LandingViewModel(SavedStateHandle())
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
awaitItem()
|
||||
viewModel.trySendAction(LandingAction.RegionOptionSelect(inputRegion))
|
||||
@@ -120,6 +137,20 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
//region Helper methods
|
||||
|
||||
private fun createViewModel(
|
||||
rememberedEmail: String? = null,
|
||||
savedStateHandle: SavedStateHandle = SavedStateHandle(),
|
||||
): LandingViewModel = LandingViewModel(
|
||||
authRepository = mockk(relaxed = true) {
|
||||
every { rememberedEmailAddress } returns rememberedEmail
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
||||
//endregion Helper methods
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = LandingState(
|
||||
emailInput = "",
|
||||
|
||||
Reference in New Issue
Block a user